@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 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.97
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.97 qingflow-app-user-mcp
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-user-mcp",
3
- "version": "0.2.0-beta.97",
3
+ "version": "0.2.0-beta.98",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b97"
7
+ version = "0.2.0b98"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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(source.get("tagItems") or base.get("tagItems"))
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
- detail_tag_items = detail_raw.get("tagItems") if isinstance(detail_raw.get("tagItems"), list) else None
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
- record = data.get("record") if isinstance(data.get("record"), dict) else {}
193
- capabilities = data.get("capabilities") if isinstance(data.get("capabilities"), dict) else {}
194
- update_schema = data.get("update_schema") if isinstance(data.get("update_schema"), dict) else {}
195
- writable_fields = update_schema.get("writable_fields") if isinstance(update_schema.get("writable_fields"), list) else []
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"Apply Status: {record.get('apply_status')}",
200
- f"Available Actions: {', '.join(str(item) for item in (capabilities.get('available_actions') or [])) or '-'}",
201
- f"Editable Fields: {len(writable_fields)}",
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 writable_fields:
204
- for item in writable_fields[:10]:
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
- blockers = update_schema.get("blockers") if isinstance(update_schema.get("blockers"), list) else []
208
- if blockers:
209
- lines.append("Update Schema Blockers:")
210
- for item in blockers:
211
- lines.append(f"- {item}")
212
- schema_warnings = update_schema.get("warnings") if isinstance(update_schema.get("warnings"), list) else []
213
- if schema_warnings:
214
- lines.append("Update Schema Warnings:")
215
- for item in schema_warnings:
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
- lines.append(f"- {item.get('code') or 'WARNING'}: {item.get('message') or ''}".rstrip())
218
- else:
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
- transfer_result = self.backend.request(
1246
- "GET",
1258
+ transfer_items, transfer_warnings, transfer_pagination = self._transfer_candidate_items(
1247
1259
  context,
1248
- f"/app/{app_key}/apply/{record_id}/transfer/member",
1249
- params={"pageNum": 1, "pageSize": 20, "auditNodeId": workflow_node_id},
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
- writable_fields.append(
1494
- self._record_tools._ready_schema_field_payload(
1495
- profile,
1496
- context,
1497
- editable_field,
1498
- ws_id=context.ws_id,
1499
- required_override=False,
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
- if current_uid is not None and item.get("uid") == current_uid:
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()
@@ -1,5 +0,0 @@
1
- """Entry point for running qingflow_mcp as a module."""
2
- from qingflow_mcp.server import main
3
-
4
- if __name__ == "__main__":
5
- main()