@josephyan/qingflow-app-user-mcp 0.2.0-beta.97 → 0.2.0-beta.98
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/service.py +23 -4
- package/src/qingflow_mcp/cli/formatters.py +33 -21
- package/src/qingflow_mcp/tools/task_context_tools.py +320 -16
- package/src/qingflow_mcp/__init__.py +0 -37
- package/src/qingflow_mcp/__main__.py +0 -5
- package/src/qingflow_mcp/backend_client.py +0 -649
- package/src/qingflow_mcp/config.py +0 -368
- package/src/qingflow_mcp/errors.py +0 -66
- package/src/qingflow_mcp/import_store.py +0 -121
- package/src/qingflow_mcp/json_types.py +0 -18
- package/src/qingflow_mcp/list_type_labels.py +0 -76
- package/src/qingflow_mcp/public_surface.py +0 -237
- package/src/qingflow_mcp/repository_store.py +0 -71
- package/src/qingflow_mcp/response_trim.py +0 -477
- package/src/qingflow_mcp/server.py +0 -212
- package/src/qingflow_mcp/server_app_builder.py +0 -557
- package/src/qingflow_mcp/server_app_user.py +0 -386
- package/src/qingflow_mcp/session_store.py +0 -289
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.98
|
|
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.98 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -445,6 +445,7 @@ class AiBuilderFacade:
|
|
|
445
445
|
base = base_result.get("result") if isinstance(base_result.get("result"), dict) else {}
|
|
446
446
|
summary = detail_result.get("summary") if isinstance(detail_result, dict) and isinstance(detail_result.get("summary"), dict) else {}
|
|
447
447
|
source = detail if detail else base
|
|
448
|
+
layout_tag_items = _select_package_layout_tag_items(detail=detail, base=base)
|
|
448
449
|
warnings: list[JSONObject] = []
|
|
449
450
|
if detail_read_error is not None:
|
|
450
451
|
warnings.append(
|
|
@@ -455,7 +456,7 @@ class AiBuilderFacade:
|
|
|
455
456
|
"http_status": detail_read_error.http_status,
|
|
456
457
|
}
|
|
457
458
|
)
|
|
458
|
-
public_items = _public_package_items_from_tag_items(
|
|
459
|
+
public_items = _public_package_items_from_tag_items(layout_tag_items)
|
|
459
460
|
item_count = summary.get("itemCount")
|
|
460
461
|
if not isinstance(item_count, int) or item_count < 0 or (item_count == 0 and public_items):
|
|
461
462
|
item_count = len(public_items)
|
|
@@ -886,9 +887,7 @@ class AiBuilderFacade:
|
|
|
886
887
|
if isinstance(current_base_result, dict) and isinstance(current_base_result.get("result"), dict)
|
|
887
888
|
else {}
|
|
888
889
|
)
|
|
889
|
-
|
|
890
|
-
base_tag_items = base_raw.get("tagItems") if isinstance(base_raw.get("tagItems"), list) else None
|
|
891
|
-
raw_tag_items = detail_tag_items if detail_tag_items else base_tag_items
|
|
890
|
+
raw_tag_items = _select_package_layout_tag_items(detail=detail_raw, base=base_raw)
|
|
892
891
|
if not isinstance(raw_tag_items, list):
|
|
893
892
|
return _failed(
|
|
894
893
|
"PACKAGE_LAYOUT_UNREADABLE",
|
|
@@ -12485,6 +12484,26 @@ def _public_package_items_from_tag_items(tag_items: Any) -> list[JSONObject]:
|
|
|
12485
12484
|
return public_items
|
|
12486
12485
|
|
|
12487
12486
|
|
|
12487
|
+
def _select_package_layout_tag_items(*, detail: Any, base: Any) -> list[Any] | None:
|
|
12488
|
+
base_tag_items = base.get("tagItems") if isinstance(base, dict) and isinstance(base.get("tagItems"), list) else None
|
|
12489
|
+
detail_tag_items = detail.get("tagItems") if isinstance(detail, dict) and isinstance(detail.get("tagItems"), list) else None
|
|
12490
|
+
if _package_tag_items_include_groups(base_tag_items):
|
|
12491
|
+
return deepcopy(base_tag_items)
|
|
12492
|
+
if _package_tag_items_include_groups(detail_tag_items):
|
|
12493
|
+
return deepcopy(detail_tag_items)
|
|
12494
|
+
if detail_tag_items is not None:
|
|
12495
|
+
return deepcopy(detail_tag_items)
|
|
12496
|
+
if base_tag_items is not None:
|
|
12497
|
+
return deepcopy(base_tag_items)
|
|
12498
|
+
return None
|
|
12499
|
+
|
|
12500
|
+
|
|
12501
|
+
def _package_tag_items_include_groups(tag_items: Any) -> bool:
|
|
12502
|
+
if not isinstance(tag_items, list):
|
|
12503
|
+
return False
|
|
12504
|
+
return any(isinstance(item, dict) and _coerce_positive_int(item.get("itemType")) == 3 for item in tag_items)
|
|
12505
|
+
|
|
12506
|
+
|
|
12488
12507
|
def _flatten_package_resource_identities(items: Any, *, public: bool) -> set[tuple[str, str]]:
|
|
12489
12508
|
flattened: set[tuple[str, str]] = set()
|
|
12490
12509
|
|
|
@@ -189,34 +189,46 @@ def _format_task_list(result: dict[str, Any]) -> str:
|
|
|
189
189
|
def _format_task_get(result: dict[str, Any]) -> str:
|
|
190
190
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
191
191
|
task = data.get("task") if isinstance(data.get("task"), dict) else {}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
record_summary = data.get("record_summary") if isinstance(data.get("record_summary"), dict) else {}
|
|
193
|
+
editable_fields = data.get("editable_fields") if isinstance(data.get("editable_fields"), list) else []
|
|
194
|
+
available_actions = data.get("available_actions") if isinstance(data.get("available_actions"), list) else []
|
|
195
|
+
extras = data.get("extras") if isinstance(data.get("extras"), dict) else {}
|
|
196
196
|
lines = [
|
|
197
197
|
f"Task: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
|
|
198
198
|
f"Node: {task.get('workflow_node_name') or '-'}",
|
|
199
|
-
f"
|
|
200
|
-
f"
|
|
201
|
-
f"
|
|
199
|
+
f"App: {task.get('app_name') or '-'}",
|
|
200
|
+
f"Initiator: {task.get('initiator') or '-'}",
|
|
201
|
+
f"Apply Status: {record_summary.get('apply_status')}",
|
|
202
|
+
f"Available Actions: {', '.join(str(item) for item in available_actions) or '-'}",
|
|
203
|
+
f"Editable Fields: {len(editable_fields)}",
|
|
202
204
|
]
|
|
203
|
-
if
|
|
204
|
-
|
|
205
|
+
core_fields = record_summary.get("core_fields") if isinstance(record_summary.get("core_fields"), dict) else {}
|
|
206
|
+
if core_fields:
|
|
207
|
+
lines.append("Core Fields:")
|
|
208
|
+
for key, value in list(core_fields.items())[:12]:
|
|
209
|
+
lines.append(f"- {key}: {value}")
|
|
210
|
+
if editable_fields:
|
|
211
|
+
lines.append("Editable Fields:")
|
|
212
|
+
for item in editable_fields[:10]:
|
|
205
213
|
if isinstance(item, dict):
|
|
206
214
|
lines.append(f"- {item.get('title') or '-'} ({item.get('kind') or 'field'})")
|
|
207
|
-
|
|
208
|
-
if
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
215
|
+
associated_reports = extras.get("associated_reports") if isinstance(extras.get("associated_reports"), dict) else {}
|
|
216
|
+
rollback_candidates = extras.get("rollback_candidates") if isinstance(extras.get("rollback_candidates"), dict) else {}
|
|
217
|
+
transfer_candidates = extras.get("transfer_candidates") if isinstance(extras.get("transfer_candidates"), dict) else {}
|
|
218
|
+
lines.append(
|
|
219
|
+
"Extras: "
|
|
220
|
+
f"reports={associated_reports.get('count', 0)}, "
|
|
221
|
+
f"rollback={rollback_candidates.get('count', 0)}, "
|
|
222
|
+
f"transfer={transfer_candidates.get('count', 0)}"
|
|
223
|
+
)
|
|
224
|
+
transfer_items = transfer_candidates.get("items") if isinstance(transfer_candidates.get("items"), list) else []
|
|
225
|
+
if transfer_items:
|
|
226
|
+
lines.append("Transfer Candidates:")
|
|
227
|
+
for item in transfer_items:
|
|
216
228
|
if isinstance(item, dict):
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
lines.append(f"- {item}")
|
|
229
|
+
display = item.get("name") or item.get("uid") or item
|
|
230
|
+
suffix = f" <{item.get('email')}>" if item.get("email") else ""
|
|
231
|
+
lines.append(f"- {display}{suffix} (uid={item.get('uid') or '-'})")
|
|
220
232
|
_append_warnings(lines, result.get("warnings"))
|
|
221
233
|
return "\n".join(lines) + "\n"
|
|
222
234
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import re
|
|
4
5
|
from typing import Any
|
|
5
6
|
from uuid import uuid4
|
|
6
7
|
|
|
@@ -218,6 +219,7 @@ class TaskContextTools(ToolBase):
|
|
|
218
219
|
include_associated_reports=include_associated_reports,
|
|
219
220
|
current_uid=session_profile.uid,
|
|
220
221
|
)
|
|
222
|
+
data = self._compact_task_get_context(data)
|
|
221
223
|
return {
|
|
222
224
|
"profile": profile,
|
|
223
225
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -1213,8 +1215,9 @@ class TaskContextTools(ToolBase):
|
|
|
1213
1215
|
f"/app/{app_key}/apply/{record_id}",
|
|
1214
1216
|
params={"role": 3, "listType": 1, "auditNodeId": workflow_node_id},
|
|
1215
1217
|
)
|
|
1218
|
+
app_name = self._task_app_name(detail, node_info)
|
|
1216
1219
|
associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
|
|
1217
|
-
associated_reports = {"visible": associated_report_visible, "count": 0, "items": []}
|
|
1220
|
+
associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": []}
|
|
1218
1221
|
if include_associated_reports and associated_report_visible:
|
|
1219
1222
|
asos_chart_list = self.backend.request(
|
|
1220
1223
|
"GET",
|
|
@@ -1229,11 +1232,21 @@ class TaskContextTools(ToolBase):
|
|
|
1229
1232
|
]
|
|
1230
1233
|
associated_reports = {
|
|
1231
1234
|
"visible": True,
|
|
1235
|
+
"loaded": True,
|
|
1232
1236
|
"count": len(associated_items),
|
|
1233
1237
|
"items": associated_items,
|
|
1234
1238
|
}
|
|
1235
1239
|
rollback_items: list[dict[str, Any]] = []
|
|
1236
1240
|
transfer_items: list[dict[str, Any]] = []
|
|
1241
|
+
transfer_warnings: list[JSONObject] = []
|
|
1242
|
+
transfer_pagination: JSONObject = {
|
|
1243
|
+
"loaded": False,
|
|
1244
|
+
"page_size": 100,
|
|
1245
|
+
"fetched_pages": 0,
|
|
1246
|
+
"reported_total": None,
|
|
1247
|
+
"page_amount": None,
|
|
1248
|
+
"truncated": False,
|
|
1249
|
+
}
|
|
1237
1250
|
if include_candidates:
|
|
1238
1251
|
rollback_result = self.backend.request(
|
|
1239
1252
|
"GET",
|
|
@@ -1242,13 +1255,13 @@ class TaskContextTools(ToolBase):
|
|
|
1242
1255
|
params={"auditNodeId": workflow_node_id},
|
|
1243
1256
|
)
|
|
1244
1257
|
rollback_items = self._rollback_candidate_items(rollback_result)
|
|
1245
|
-
|
|
1246
|
-
"GET",
|
|
1258
|
+
transfer_items, transfer_warnings, transfer_pagination = self._transfer_candidate_items(
|
|
1247
1259
|
context,
|
|
1248
|
-
|
|
1249
|
-
|
|
1260
|
+
app_key=app_key,
|
|
1261
|
+
record_id=record_id,
|
|
1262
|
+
workflow_node_id=workflow_node_id,
|
|
1263
|
+
current_uid=current_uid,
|
|
1250
1264
|
)
|
|
1251
|
-
transfer_items = self._filter_transfer_members(_approval_page_items(transfer_result), current_uid=current_uid)
|
|
1252
1265
|
|
|
1253
1266
|
update_schema_state = self._build_task_update_schema(
|
|
1254
1267
|
profile=profile,
|
|
@@ -1275,6 +1288,7 @@ class TaskContextTools(ToolBase):
|
|
|
1275
1288
|
return {
|
|
1276
1289
|
"task": {
|
|
1277
1290
|
"app_key": app_key,
|
|
1291
|
+
"app_name": app_name,
|
|
1278
1292
|
"record_id": record_id,
|
|
1279
1293
|
"workflow_node_id": workflow_node_id,
|
|
1280
1294
|
"workflow_node_name": node_info.get("auditNodeName") or node_info.get("nodeName"),
|
|
@@ -1306,6 +1320,9 @@ class TaskContextTools(ToolBase):
|
|
|
1306
1320
|
"candidates": {
|
|
1307
1321
|
"rollback_nodes": rollback_items,
|
|
1308
1322
|
"transfer_members": transfer_items,
|
|
1323
|
+
"loaded": include_candidates,
|
|
1324
|
+
"transfer_pagination": transfer_pagination,
|
|
1325
|
+
"warnings": transfer_warnings,
|
|
1309
1326
|
},
|
|
1310
1327
|
"workflow_log_summary": {
|
|
1311
1328
|
"visible": visibility["audit_record_visible"],
|
|
@@ -1316,6 +1333,219 @@ class TaskContextTools(ToolBase):
|
|
|
1316
1333
|
"update_schema": update_schema,
|
|
1317
1334
|
}
|
|
1318
1335
|
|
|
1336
|
+
def _compact_task_get_context(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
1337
|
+
task = data.get("task") if isinstance(data.get("task"), dict) else {}
|
|
1338
|
+
record = data.get("record") if isinstance(data.get("record"), dict) else {}
|
|
1339
|
+
capabilities = data.get("capabilities") if isinstance(data.get("capabilities"), dict) else {}
|
|
1340
|
+
update_schema = data.get("update_schema") if isinstance(data.get("update_schema"), dict) else {}
|
|
1341
|
+
associated_reports = data.get("associated_reports") if isinstance(data.get("associated_reports"), dict) else {}
|
|
1342
|
+
candidates = data.get("candidates") if isinstance(data.get("candidates"), dict) else {}
|
|
1343
|
+
workflow_log = data.get("workflow_log_summary") if isinstance(data.get("workflow_log_summary"), dict) else {}
|
|
1344
|
+
|
|
1345
|
+
available_actions = [
|
|
1346
|
+
str(item)
|
|
1347
|
+
for item in (capabilities.get("available_actions") or [])
|
|
1348
|
+
if str(item).strip()
|
|
1349
|
+
]
|
|
1350
|
+
writable_fields = update_schema.get("writable_fields") if isinstance(update_schema.get("writable_fields"), list) else []
|
|
1351
|
+
rollback_items = [
|
|
1352
|
+
self._compact_rollback_candidate(item)
|
|
1353
|
+
for item in (candidates.get("rollback_nodes") or [])
|
|
1354
|
+
if isinstance(item, dict)
|
|
1355
|
+
]
|
|
1356
|
+
transfer_items = [
|
|
1357
|
+
self._compact_transfer_member(item)
|
|
1358
|
+
for item in (candidates.get("transfer_members") or [])
|
|
1359
|
+
if isinstance(item, dict)
|
|
1360
|
+
]
|
|
1361
|
+
associated_items = [
|
|
1362
|
+
self._compact_associated_report(item)
|
|
1363
|
+
for item in (associated_reports.get("items") or [])
|
|
1364
|
+
if isinstance(item, dict)
|
|
1365
|
+
]
|
|
1366
|
+
transfer_pagination = candidates.get("transfer_pagination") if isinstance(candidates.get("transfer_pagination"), dict) else {}
|
|
1367
|
+
compact: dict[str, Any] = {
|
|
1368
|
+
"task": {
|
|
1369
|
+
"app_key": task.get("app_key"),
|
|
1370
|
+
"app_name": task.get("app_name"),
|
|
1371
|
+
"record_id": task.get("record_id"),
|
|
1372
|
+
"workflow_node_id": task.get("workflow_node_id"),
|
|
1373
|
+
"workflow_node_name": task.get("workflow_node_name"),
|
|
1374
|
+
"initiator": record.get("apply_user"),
|
|
1375
|
+
"actionable": task.get("actionable"),
|
|
1376
|
+
},
|
|
1377
|
+
"record_summary": {
|
|
1378
|
+
"apply_status": record.get("apply_status"),
|
|
1379
|
+
"apply_num": record.get("apply_num"),
|
|
1380
|
+
"custom_apply_num": record.get("custom_apply_num"),
|
|
1381
|
+
"apply_time": record.get("apply_time"),
|
|
1382
|
+
"last_update_time": record.get("last_update_time"),
|
|
1383
|
+
"core_fields": self._task_record_core_fields(record.get("answers") or []),
|
|
1384
|
+
},
|
|
1385
|
+
"available_actions": available_actions,
|
|
1386
|
+
"editable_fields": [
|
|
1387
|
+
self._compact_task_editable_field(item, update_schema)
|
|
1388
|
+
for item in writable_fields
|
|
1389
|
+
if isinstance(item, dict)
|
|
1390
|
+
],
|
|
1391
|
+
"extras": {
|
|
1392
|
+
"workflow_log": {
|
|
1393
|
+
"available": bool(workflow_log.get("available")),
|
|
1394
|
+
"qrobot_log_visible": bool(workflow_log.get("qrobot_log_visible")),
|
|
1395
|
+
"history_count": workflow_log.get("history_count"),
|
|
1396
|
+
},
|
|
1397
|
+
"associated_reports": {
|
|
1398
|
+
"available": bool(associated_reports.get("visible")),
|
|
1399
|
+
"loaded": bool(associated_reports.get("loaded")),
|
|
1400
|
+
"count": len(associated_items),
|
|
1401
|
+
"items": associated_items,
|
|
1402
|
+
},
|
|
1403
|
+
"rollback_candidates": {
|
|
1404
|
+
"available": "rollback" in available_actions,
|
|
1405
|
+
"loaded": bool(candidates.get("loaded")),
|
|
1406
|
+
"count": len(rollback_items),
|
|
1407
|
+
"items": rollback_items,
|
|
1408
|
+
},
|
|
1409
|
+
"transfer_candidates": {
|
|
1410
|
+
"available": "transfer" in available_actions,
|
|
1411
|
+
"loaded": bool(transfer_pagination.get("loaded")),
|
|
1412
|
+
"count": len(transfer_items),
|
|
1413
|
+
"items": transfer_items,
|
|
1414
|
+
"pagination": transfer_pagination,
|
|
1415
|
+
"warnings": candidates.get("warnings") or [],
|
|
1416
|
+
},
|
|
1417
|
+
},
|
|
1418
|
+
}
|
|
1419
|
+
action_metadata = self._compact_task_action_metadata(capabilities)
|
|
1420
|
+
if action_metadata:
|
|
1421
|
+
compact["action_metadata"] = action_metadata
|
|
1422
|
+
editable_metadata = self._compact_task_editable_metadata(update_schema)
|
|
1423
|
+
if editable_metadata:
|
|
1424
|
+
compact["editable_metadata"] = editable_metadata
|
|
1425
|
+
return compact
|
|
1426
|
+
|
|
1427
|
+
def _compact_task_action_metadata(self, capabilities: dict[str, Any]) -> dict[str, Any]:
|
|
1428
|
+
constraints = capabilities.get("action_constraints") if isinstance(capabilities.get("action_constraints"), dict) else {}
|
|
1429
|
+
metadata: dict[str, Any] = {}
|
|
1430
|
+
feedback_required_for = constraints.get("feedback_required_for") if isinstance(constraints.get("feedback_required_for"), list) else []
|
|
1431
|
+
if feedback_required_for:
|
|
1432
|
+
metadata["feedback_required_for"] = feedback_required_for
|
|
1433
|
+
visible_but_unimplemented = capabilities.get("visible_but_unimplemented_actions")
|
|
1434
|
+
if visible_but_unimplemented:
|
|
1435
|
+
metadata["visible_but_unimplemented_actions"] = visible_but_unimplemented
|
|
1436
|
+
if capabilities.get("save_only_source"):
|
|
1437
|
+
metadata["save_only_source"] = capabilities.get("save_only_source")
|
|
1438
|
+
if capabilities.get("warnings"):
|
|
1439
|
+
metadata["warnings"] = capabilities.get("warnings")
|
|
1440
|
+
return metadata
|
|
1441
|
+
|
|
1442
|
+
def _compact_task_editable_metadata(self, update_schema: dict[str, Any]) -> dict[str, Any]:
|
|
1443
|
+
metadata: dict[str, Any] = {}
|
|
1444
|
+
blockers = update_schema.get("blockers") if isinstance(update_schema.get("blockers"), list) else []
|
|
1445
|
+
warnings = update_schema.get("warnings") if isinstance(update_schema.get("warnings"), list) else []
|
|
1446
|
+
if blockers:
|
|
1447
|
+
metadata["blockers"] = blockers
|
|
1448
|
+
if warnings:
|
|
1449
|
+
metadata["warnings"] = warnings
|
|
1450
|
+
return metadata
|
|
1451
|
+
|
|
1452
|
+
def _task_app_name(self, detail: dict[str, Any], node_info: dict[str, Any]) -> Any:
|
|
1453
|
+
for source in (detail, node_info):
|
|
1454
|
+
for key in ("formTitle", "appName", "worksheetName", "appTitle"):
|
|
1455
|
+
value = source.get(key)
|
|
1456
|
+
if value not in (None, ""):
|
|
1457
|
+
return value
|
|
1458
|
+
return None
|
|
1459
|
+
|
|
1460
|
+
def _task_record_core_fields(self, answers: Any, *, limit: int = 12) -> dict[str, Any]:
|
|
1461
|
+
if not isinstance(answers, list):
|
|
1462
|
+
return {}
|
|
1463
|
+
core_fields: dict[str, Any] = {}
|
|
1464
|
+
for answer in answers:
|
|
1465
|
+
if not isinstance(answer, dict):
|
|
1466
|
+
continue
|
|
1467
|
+
title = answer.get("queTitle") or answer.get("title") or answer.get("fieldName")
|
|
1468
|
+
if not title:
|
|
1469
|
+
que_id = answer.get("queId")
|
|
1470
|
+
title = f"field_{que_id}" if que_id not in (None, "") else None
|
|
1471
|
+
if not title:
|
|
1472
|
+
continue
|
|
1473
|
+
table_values = answer.get("tableValues") if isinstance(answer.get("tableValues"), list) else []
|
|
1474
|
+
if table_values:
|
|
1475
|
+
value: Any = f"子表格 {len(table_values)} 行"
|
|
1476
|
+
else:
|
|
1477
|
+
values = self._extract_answer_values(answer)
|
|
1478
|
+
if not values:
|
|
1479
|
+
continue
|
|
1480
|
+
value = values[0] if len(values) == 1 else values
|
|
1481
|
+
if value in (None, "", []):
|
|
1482
|
+
continue
|
|
1483
|
+
core_fields[str(title)] = self._compact_task_value(value)
|
|
1484
|
+
if len(core_fields) >= limit:
|
|
1485
|
+
break
|
|
1486
|
+
return core_fields
|
|
1487
|
+
|
|
1488
|
+
def _compact_task_value(self, value: Any) -> Any:
|
|
1489
|
+
if isinstance(value, list):
|
|
1490
|
+
return [self._compact_task_value(item) for item in value[:8]]
|
|
1491
|
+
text = re.sub(r"<[^>]+>", " ", str(value))
|
|
1492
|
+
text = re.sub(r"\s+", " ", text).strip()
|
|
1493
|
+
if len(text) <= 160:
|
|
1494
|
+
return text
|
|
1495
|
+
return text[:157].rstrip() + "..."
|
|
1496
|
+
|
|
1497
|
+
def _compact_task_editable_field(self, field: dict[str, Any], update_schema: dict[str, Any]) -> dict[str, Any]:
|
|
1498
|
+
payload_template = update_schema.get("payload_template") if isinstance(update_schema.get("payload_template"), dict) else {}
|
|
1499
|
+
title = field.get("title")
|
|
1500
|
+
compact: dict[str, Any] = {}
|
|
1501
|
+
for key in ("field_id", "title", "kind", "required", "candidate_hint"):
|
|
1502
|
+
if key in field:
|
|
1503
|
+
compact[key] = field.get(key)
|
|
1504
|
+
if title in payload_template:
|
|
1505
|
+
compact["template"] = payload_template.get(title)
|
|
1506
|
+
return compact
|
|
1507
|
+
|
|
1508
|
+
def _compact_associated_report(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
1509
|
+
return {
|
|
1510
|
+
key: value
|
|
1511
|
+
for key, value in {
|
|
1512
|
+
"report_id": item.get("report_id"),
|
|
1513
|
+
"chart_key": item.get("chart_key"),
|
|
1514
|
+
"chart_name": item.get("chart_name"),
|
|
1515
|
+
"graph_type": item.get("graph_type"),
|
|
1516
|
+
"source_type": item.get("source_type"),
|
|
1517
|
+
"target_app_key": item.get("target_app_key"),
|
|
1518
|
+
"target_app_name": item.get("target_app_name"),
|
|
1519
|
+
}.items()
|
|
1520
|
+
if value not in (None, "", [])
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
def _compact_rollback_candidate(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
1524
|
+
return {
|
|
1525
|
+
key: value
|
|
1526
|
+
for key, value in {
|
|
1527
|
+
"workflow_node_id": item.get("auditNodeId") or item.get("nodeId"),
|
|
1528
|
+
"workflow_node_name": item.get("auditNodeName") or item.get("nodeName"),
|
|
1529
|
+
}.items()
|
|
1530
|
+
if value not in (None, "", [])
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
def _compact_transfer_member(self, item: dict[str, Any]) -> dict[str, Any]:
|
|
1534
|
+
uid = item.get("uid")
|
|
1535
|
+
if uid is None:
|
|
1536
|
+
uid = item.get("userId") or item.get("memberId") or item.get("id")
|
|
1537
|
+
return {
|
|
1538
|
+
key: value
|
|
1539
|
+
for key, value in {
|
|
1540
|
+
"uid": uid,
|
|
1541
|
+
"name": item.get("name") or item.get("userName") or item.get("memberName") or item.get("realName"),
|
|
1542
|
+
"email": item.get("email") or item.get("mail"),
|
|
1543
|
+
"department_id": item.get("departmentId") or item.get("deptId"),
|
|
1544
|
+
"department_name": item.get("departmentName") or item.get("deptName"),
|
|
1545
|
+
}.items()
|
|
1546
|
+
if value not in (None, "", [])
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1319
1549
|
def _normalize_task_item(self, raw: dict[str, Any], *, task_box: str, flow_status: str) -> dict[str, Any]:
|
|
1320
1550
|
app_key = raw.get("appKey") or raw.get("app_key")
|
|
1321
1551
|
record_id = raw.get("rowRecordId") or raw.get("recordId") or raw.get("applyId")
|
|
@@ -1490,16 +1720,16 @@ class TaskContextTools(ToolBase):
|
|
|
1490
1720
|
write_hints = self._record_tools._schema_write_hints(editable_field)
|
|
1491
1721
|
if not bool(write_hints.get("writable")):
|
|
1492
1722
|
continue
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
linkage_payloads_by_field_id=linkage_payloads_by_field_id,
|
|
1501
|
-
)
|
|
1723
|
+
writable_field = self._record_tools._ready_schema_field_payload(
|
|
1724
|
+
profile,
|
|
1725
|
+
context,
|
|
1726
|
+
editable_field,
|
|
1727
|
+
ws_id=context.ws_id,
|
|
1728
|
+
required_override=False,
|
|
1729
|
+
linkage_payloads_by_field_id=linkage_payloads_by_field_id,
|
|
1502
1730
|
)
|
|
1731
|
+
writable_field.setdefault("field_id", editable_field.que_id)
|
|
1732
|
+
writable_fields.append(writable_field)
|
|
1503
1733
|
blockers: list[str] = []
|
|
1504
1734
|
if not writable_fields:
|
|
1505
1735
|
blockers.append("NO_TASK_EDITABLE_FIELDS")
|
|
@@ -1879,11 +2109,85 @@ class TaskContextTools(ToolBase):
|
|
|
1879
2109
|
for item in items:
|
|
1880
2110
|
if not isinstance(item, dict):
|
|
1881
2111
|
continue
|
|
1882
|
-
|
|
2112
|
+
uid = _coerce_count(item.get("uid") or item.get("userId") or item.get("memberId") or item.get("id"))
|
|
2113
|
+
if current_uid is not None and uid == current_uid:
|
|
1883
2114
|
continue
|
|
1884
2115
|
filtered.append(item)
|
|
1885
2116
|
return filtered
|
|
1886
2117
|
|
|
2118
|
+
def _transfer_candidate_items(
|
|
2119
|
+
self,
|
|
2120
|
+
context: BackendRequestContext,
|
|
2121
|
+
*,
|
|
2122
|
+
app_key: str,
|
|
2123
|
+
record_id: int,
|
|
2124
|
+
workflow_node_id: int,
|
|
2125
|
+
current_uid: int | None,
|
|
2126
|
+
) -> tuple[list[dict[str, Any]], list[JSONObject], JSONObject]:
|
|
2127
|
+
page_size = 100
|
|
2128
|
+
max_pages = 100
|
|
2129
|
+
page_num = 1
|
|
2130
|
+
fetched_pages = 0
|
|
2131
|
+
fetched_raw_count = 0
|
|
2132
|
+
page_amount: int | None = None
|
|
2133
|
+
reported_total: int | None = None
|
|
2134
|
+
items: list[dict[str, Any]] = []
|
|
2135
|
+
seen_member_keys: set[str] = set()
|
|
2136
|
+
warnings: list[JSONObject] = []
|
|
2137
|
+
|
|
2138
|
+
while page_num <= max_pages:
|
|
2139
|
+
result = self.backend.request(
|
|
2140
|
+
"GET",
|
|
2141
|
+
context,
|
|
2142
|
+
f"/app/{app_key}/apply/{record_id}/transfer/member",
|
|
2143
|
+
params={"pageNum": page_num, "pageSize": page_size, "auditNodeId": workflow_node_id},
|
|
2144
|
+
)
|
|
2145
|
+
fetched_pages += 1
|
|
2146
|
+
raw_items = _approval_page_items(result)
|
|
2147
|
+
fetched_raw_count += len(raw_items)
|
|
2148
|
+
if page_amount is None:
|
|
2149
|
+
page_amount = _coerce_count(_approval_page_amount(result))
|
|
2150
|
+
if reported_total is None:
|
|
2151
|
+
reported_total = _coerce_count(_approval_page_total(result))
|
|
2152
|
+
for item in self._filter_transfer_members(raw_items, current_uid=current_uid):
|
|
2153
|
+
member_key = self._transfer_member_dedupe_key(item)
|
|
2154
|
+
if member_key in seen_member_keys:
|
|
2155
|
+
continue
|
|
2156
|
+
seen_member_keys.add(member_key)
|
|
2157
|
+
items.append(item)
|
|
2158
|
+
if not raw_items:
|
|
2159
|
+
break
|
|
2160
|
+
if page_amount is not None and page_num >= page_amount:
|
|
2161
|
+
break
|
|
2162
|
+
if reported_total is not None and fetched_raw_count >= reported_total:
|
|
2163
|
+
break
|
|
2164
|
+
page_num += 1
|
|
2165
|
+
truncated = page_num > max_pages
|
|
2166
|
+
if truncated:
|
|
2167
|
+
warnings.append(
|
|
2168
|
+
{
|
|
2169
|
+
"code": "TRANSFER_CANDIDATES_TRUNCATED",
|
|
2170
|
+
"message": "transfer candidates reached the MCP safety page cap; returned candidates may be incomplete.",
|
|
2171
|
+
"max_pages": max_pages,
|
|
2172
|
+
"page_size": page_size,
|
|
2173
|
+
}
|
|
2174
|
+
)
|
|
2175
|
+
pagination: JSONObject = {
|
|
2176
|
+
"loaded": True,
|
|
2177
|
+
"page_size": page_size,
|
|
2178
|
+
"fetched_pages": fetched_pages,
|
|
2179
|
+
"reported_total": reported_total,
|
|
2180
|
+
"page_amount": page_amount,
|
|
2181
|
+
"truncated": truncated,
|
|
2182
|
+
}
|
|
2183
|
+
return items, warnings, pagination
|
|
2184
|
+
|
|
2185
|
+
def _transfer_member_dedupe_key(self, item: dict[str, Any]) -> str:
|
|
2186
|
+
uid = item.get("uid") or item.get("userId") or item.get("memberId") or item.get("id")
|
|
2187
|
+
if uid not in (None, ""):
|
|
2188
|
+
return f"uid:{uid}"
|
|
2189
|
+
return json.dumps(item, ensure_ascii=False, sort_keys=True, default=str)
|
|
2190
|
+
|
|
1887
2191
|
def _find_associated_report(self, task_context: dict[str, Any], report_id: int) -> dict[str, Any] | None:
|
|
1888
2192
|
associated_reports = ((task_context.get("associated_reports") or {}).get("items") or [])
|
|
1889
2193
|
for item in associated_reports:
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from importlib.metadata import PackageNotFoundError, packages_distributions, version as _dist_version
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
__all__ = ["__version__"]
|
|
7
|
-
|
|
8
|
-
_FALLBACK_VERSION = "0.2.0b97"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _resolve_local_pyproject_version() -> str | None:
|
|
12
|
-
module_path = Path(__file__).resolve()
|
|
13
|
-
for parent in module_path.parents:
|
|
14
|
-
candidate = parent / "pyproject.toml"
|
|
15
|
-
if not candidate.is_file():
|
|
16
|
-
continue
|
|
17
|
-
for line in candidate.read_text(encoding="utf-8").splitlines():
|
|
18
|
-
stripped = line.strip()
|
|
19
|
-
if stripped.startswith("version = "):
|
|
20
|
-
return stripped.split("=", 1)[1].strip().strip('"')
|
|
21
|
-
break
|
|
22
|
-
return None
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _resolve_runtime_version() -> str:
|
|
26
|
-
local_version = _resolve_local_pyproject_version()
|
|
27
|
-
if local_version:
|
|
28
|
-
return local_version
|
|
29
|
-
for dist_name in packages_distributions().get("qingflow_mcp", []):
|
|
30
|
-
try:
|
|
31
|
-
return _dist_version(dist_name)
|
|
32
|
-
except PackageNotFoundError:
|
|
33
|
-
continue
|
|
34
|
-
return _FALLBACK_VERSION
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
__version__ = _resolve_runtime_version()
|