@qingflow-tech/qingflow-app-user-mcp 1.0.11 → 1.0.13
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 +9 -3
- package/docs/local-agent-install.md +54 -3
- package/entry_point.py +1 -1
- package/npm/bin/qingflow-skills.mjs +5 -0
- package/npm/lib/runtime.mjs +304 -13
- package/npm/scripts/postinstall.mjs +1 -5
- package/package.json +3 -2
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-builder/SKILL.md +255 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +149 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +107 -0
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +72 -0
- package/skills/qingflow-app-builder/references/update-views.md +284 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/skills/qingflow-app-user/SKILL.md +12 -11
- package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
- package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
- package/skills/qingflow-app-user/references/record-patterns.md +5 -5
- package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
- package/skills/qingflow-mcp-setup/SKILL.md +113 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +62 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-record-analysis/SKILL.md +6 -7
- package/skills/qingflow-record-analysis/manifest.yaml +10 -0
- package/skills/qingflow-record-delete/SKILL.md +5 -3
- package/skills/qingflow-record-import/SKILL.md +6 -2
- package/skills/qingflow-record-insert/SKILL.md +48 -4
- package/skills/qingflow-record-insert/manifest.yaml +6 -0
- package/skills/qingflow-record-update/SKILL.md +36 -24
- package/skills/qingflow-task-ops/SKILL.md +25 -25
- package/skills/qingflow-task-ops/references/environments.md +0 -1
- package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
- package/src/qingflow_mcp/__main__.py +6 -2
- package/src/qingflow_mcp/builder_facade/models.py +11 -0
- package/src/qingflow_mcp/builder_facade/service.py +1488 -288
- package/src/qingflow_mcp/cli/commands/builder.py +2 -2
- package/src/qingflow_mcp/cli/commands/exports.py +2 -2
- package/src/qingflow_mcp/cli/commands/imports.py +1 -1
- package/src/qingflow_mcp/cli/commands/record.py +91 -19
- package/src/qingflow_mcp/cli/context.py +0 -3
- package/src/qingflow_mcp/cli/formatters.py +206 -7
- package/src/qingflow_mcp/cli/main.py +47 -3
- package/src/qingflow_mcp/errors.py +43 -2
- package/src/qingflow_mcp/public_surface.py +21 -15
- package/src/qingflow_mcp/response_trim.py +74 -13
- package/src/qingflow_mcp/server.py +11 -9
- package/src/qingflow_mcp/server_app_builder.py +3 -2
- package/src/qingflow_mcp/server_app_user.py +19 -13
- package/src/qingflow_mcp/session_store.py +11 -7
- package/src/qingflow_mcp/solution/executor.py +112 -15
- package/src/qingflow_mcp/tools/ai_builder_tools.py +36 -11
- package/src/qingflow_mcp/tools/app_tools.py +184 -43
- package/src/qingflow_mcp/tools/approval_tools.py +196 -34
- package/src/qingflow_mcp/tools/auth_tools.py +92 -16
- package/src/qingflow_mcp/tools/code_block_tools.py +298 -40
- package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
- package/src/qingflow_mcp/tools/directory_tools.py +236 -72
- package/src/qingflow_mcp/tools/export_tools.py +244 -34
- package/src/qingflow_mcp/tools/file_tools.py +7 -3
- package/src/qingflow_mcp/tools/import_tools.py +336 -49
- package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
- package/src/qingflow_mcp/tools/package_tools.py +118 -6
- package/src/qingflow_mcp/tools/portal_tools.py +39 -3
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
- package/src/qingflow_mcp/tools/record_tools.py +1067 -349
- package/src/qingflow_mcp/tools/resource_read_tools.py +188 -39
- package/src/qingflow_mcp/tools/role_tools.py +80 -9
- package/src/qingflow_mcp/tools/solution_tools.py +57 -15
- package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
- package/src/qingflow_mcp/tools/task_tools.py +113 -29
- package/src/qingflow_mcp/tools/view_tools.py +106 -3
- package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
- package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
|
@@ -9,12 +9,12 @@ from mcp.server.fastmcp import FastMCP
|
|
|
9
9
|
|
|
10
10
|
from ..backend_client import BackendRequestContext
|
|
11
11
|
from ..config import DEFAULT_PROFILE
|
|
12
|
-
from ..errors import QingflowApiError, raise_tool_error
|
|
12
|
+
from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error, message_looks_like_invalid_token, raise_tool_error
|
|
13
13
|
from ..id_utils import ids_equal, normalize_positive_id_int, normalize_positive_id_text, stringify_backend_id
|
|
14
14
|
from ..json_types import JSONObject
|
|
15
15
|
from .approval_tools import ApprovalTools, _approval_page_amount, _approval_page_items, _approval_page_total
|
|
16
16
|
from .base import ToolBase, tool_cn_name
|
|
17
|
-
from .qingbi_report_tools import _qingbi_base_url
|
|
17
|
+
from .qingbi_report_tools import _qingbi_base_url, _should_retry_asos_data
|
|
18
18
|
from .record_tools import (
|
|
19
19
|
FieldIndex,
|
|
20
20
|
LAYOUT_ONLY_QUE_TYPES,
|
|
@@ -271,6 +271,7 @@ class TaskContextTools(ToolBase):
|
|
|
271
271
|
resolved_app_key = str(locator["app_key"])
|
|
272
272
|
resolved_record_id = int(locator["record_id"])
|
|
273
273
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
274
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
274
275
|
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
275
276
|
data = self._build_task_context(
|
|
276
277
|
profile=profile,
|
|
@@ -278,10 +279,12 @@ class TaskContextTools(ToolBase):
|
|
|
278
279
|
app_key=resolved_app_key,
|
|
279
280
|
record_id=resolved_record_id,
|
|
280
281
|
workflow_node_id=resolved_workflow_node_id,
|
|
282
|
+
task_box=resolved_task_box,
|
|
281
283
|
include_candidates=include_candidates,
|
|
282
284
|
include_associated_reports=include_associated_reports,
|
|
283
285
|
current_uid=session_profile.uid,
|
|
284
286
|
)
|
|
287
|
+
context_warnings = data.get("warnings") if isinstance(data.get("warnings"), list) else []
|
|
285
288
|
data = self._compact_task_get_context(data)
|
|
286
289
|
task_payload = data.get("task")
|
|
287
290
|
if isinstance(task_payload, dict) and task_id_text is not None:
|
|
@@ -291,7 +294,7 @@ class TaskContextTools(ToolBase):
|
|
|
291
294
|
"ws_id": session_profile.selected_ws_id,
|
|
292
295
|
"ok": True,
|
|
293
296
|
"request_route": self._request_route_payload(context),
|
|
294
|
-
"warnings":
|
|
297
|
+
"warnings": context_warnings,
|
|
295
298
|
"output_profile": "normal",
|
|
296
299
|
"data": data,
|
|
297
300
|
}
|
|
@@ -368,8 +371,63 @@ class TaskContextTools(ToolBase):
|
|
|
368
371
|
resolved_record_id = int(locator["record_id"])
|
|
369
372
|
resolved_record_id_text = str(locator["record_id_text"] or "")
|
|
370
373
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
374
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
371
375
|
record_id_text = resolved_record_id_text
|
|
372
376
|
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
377
|
+
if normalized_action == "urge":
|
|
378
|
+
raw = self._execute_task_action(
|
|
379
|
+
profile=profile,
|
|
380
|
+
app_key=resolved_app_key,
|
|
381
|
+
record_id=resolved_record_id,
|
|
382
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
383
|
+
normalized_action=normalized_action,
|
|
384
|
+
payload=body,
|
|
385
|
+
prepared_fields=None,
|
|
386
|
+
)
|
|
387
|
+
verification, verification_warnings = self._verify_task_action_runtime(
|
|
388
|
+
profile=profile,
|
|
389
|
+
context=context,
|
|
390
|
+
app_key=resolved_app_key,
|
|
391
|
+
record_id=resolved_record_id,
|
|
392
|
+
workflow_node_id=resolved_workflow_node_id,
|
|
393
|
+
action=normalized_action,
|
|
394
|
+
before_apply_status=None,
|
|
395
|
+
runtime_baseline=None,
|
|
396
|
+
)
|
|
397
|
+
result = {
|
|
398
|
+
"profile": raw.get("profile", profile),
|
|
399
|
+
"ws_id": raw.get("ws_id", session_profile.selected_ws_id),
|
|
400
|
+
"ok": bool(raw.get("ok", True)),
|
|
401
|
+
"status": "success",
|
|
402
|
+
"error_code": None,
|
|
403
|
+
"action_executed": True,
|
|
404
|
+
"safe_to_retry": False,
|
|
405
|
+
"request_route": raw.get("request_route") or self._request_route_payload(context),
|
|
406
|
+
"warnings": verification_warnings,
|
|
407
|
+
"verification": verification,
|
|
408
|
+
"output_profile": "normal",
|
|
409
|
+
"data": {
|
|
410
|
+
"action": normalized_action,
|
|
411
|
+
"resource": {
|
|
412
|
+
"app_key": resolved_app_key,
|
|
413
|
+
"record_id": record_id_text,
|
|
414
|
+
"workflow_node_id": resolved_workflow_node_id,
|
|
415
|
+
},
|
|
416
|
+
"selection": {"action": normalized_action},
|
|
417
|
+
"result": raw.get("result"),
|
|
418
|
+
"human_review": True,
|
|
419
|
+
"field_update_applied": False,
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
if task_id_text is not None:
|
|
423
|
+
result["data"]["resource"]["task_id"] = task_id_text
|
|
424
|
+
return result
|
|
425
|
+
if resolved_task_box != "todo":
|
|
426
|
+
raise_tool_error(
|
|
427
|
+
QingflowApiError.config_error(
|
|
428
|
+
f"task_id={task_id_text or ''} resolved to task_box='{resolved_task_box}', but task_action_execute can only execute current todo tasks"
|
|
429
|
+
)
|
|
430
|
+
)
|
|
373
431
|
try:
|
|
374
432
|
task_context = self._build_task_context(
|
|
375
433
|
profile=profile,
|
|
@@ -377,12 +435,13 @@ class TaskContextTools(ToolBase):
|
|
|
377
435
|
app_key=resolved_app_key,
|
|
378
436
|
record_id=resolved_record_id,
|
|
379
437
|
workflow_node_id=resolved_workflow_node_id,
|
|
438
|
+
task_box=resolved_task_box,
|
|
380
439
|
include_candidates=False,
|
|
381
440
|
include_associated_reports=False,
|
|
382
441
|
current_uid=session_profile.uid,
|
|
383
442
|
)
|
|
384
443
|
except QingflowApiError as error:
|
|
385
|
-
if error
|
|
444
|
+
if backend_code_int(error) == 46001:
|
|
386
445
|
return self._task_action_visibility_unverified_response(
|
|
387
446
|
profile=profile,
|
|
388
447
|
session_profile=session_profile,
|
|
@@ -470,7 +529,7 @@ class TaskContextTools(ToolBase):
|
|
|
470
529
|
prepared_fields=prepared_fields,
|
|
471
530
|
)
|
|
472
531
|
except QingflowApiError as error:
|
|
473
|
-
if error
|
|
532
|
+
if backend_code_int(error) == 46001:
|
|
474
533
|
return self._task_action_visibility_unverified_response(
|
|
475
534
|
profile=profile,
|
|
476
535
|
session_profile=session_profile,
|
|
@@ -518,6 +577,8 @@ class TaskContextTools(ToolBase):
|
|
|
518
577
|
"ok": bool(raw.get("ok", True)) and status != "failed",
|
|
519
578
|
"status": status,
|
|
520
579
|
"error_code": error_code,
|
|
580
|
+
"action_executed": True,
|
|
581
|
+
"safe_to_retry": False,
|
|
521
582
|
"request_route": raw.get("request_route") or self._request_route_payload(context),
|
|
522
583
|
"warnings": warnings,
|
|
523
584
|
"verification": verification,
|
|
@@ -680,6 +741,8 @@ class TaskContextTools(ToolBase):
|
|
|
680
741
|
"http_status": error.http_status,
|
|
681
742
|
"backend_code": error.backend_code,
|
|
682
743
|
"category": error.category,
|
|
744
|
+
"request_id": error.request_id,
|
|
745
|
+
"message": error.message,
|
|
683
746
|
}
|
|
684
747
|
|
|
685
748
|
log_items: list[dict[str, Any]] = []
|
|
@@ -707,6 +770,8 @@ class TaskContextTools(ToolBase):
|
|
|
707
770
|
"http_status": error.http_status,
|
|
708
771
|
"backend_code": error.backend_code,
|
|
709
772
|
"category": error.category,
|
|
773
|
+
"request_id": error.request_id,
|
|
774
|
+
"message": error.message,
|
|
710
775
|
}
|
|
711
776
|
|
|
712
777
|
todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
|
|
@@ -756,7 +821,7 @@ class TaskContextTools(ToolBase):
|
|
|
756
821
|
runtime_consumed_after_action = bool(
|
|
757
822
|
runtime_verified
|
|
758
823
|
and isinstance(record_state_error, dict)
|
|
759
|
-
and record_state_error.get("backend_code") == 46001
|
|
824
|
+
and backend_code_value_int(record_state_error.get("backend_code")) == 46001
|
|
760
825
|
)
|
|
761
826
|
if runtime_consumed_after_action:
|
|
762
827
|
verification["record_state_scope"] = "current_node_runtime"
|
|
@@ -771,6 +836,28 @@ class TaskContextTools(ToolBase):
|
|
|
771
836
|
),
|
|
772
837
|
}
|
|
773
838
|
)
|
|
839
|
+
permission_blocked_sources: list[str] = []
|
|
840
|
+
if (
|
|
841
|
+
isinstance(verification.get("record_state_error"), dict)
|
|
842
|
+
and _is_permission_context_error_payload(verification["record_state_error"])
|
|
843
|
+
):
|
|
844
|
+
permission_blocked_sources.append("record_state")
|
|
845
|
+
if (
|
|
846
|
+
isinstance(verification.get("workflow_log_error"), dict)
|
|
847
|
+
and _is_permission_context_error_payload(verification["workflow_log_error"])
|
|
848
|
+
):
|
|
849
|
+
permission_blocked_sources.append("workflow_log")
|
|
850
|
+
if permission_blocked_sources:
|
|
851
|
+
warnings.append(
|
|
852
|
+
{
|
|
853
|
+
"code": "TASK_ACTION_VERIFICATION_PERMISSION_UNAVAILABLE",
|
|
854
|
+
"message": (
|
|
855
|
+
"task action executed, but some post-action verification reads are unavailable "
|
|
856
|
+
"in this permission context; do not treat the verification read failure as action denial."
|
|
857
|
+
),
|
|
858
|
+
"sources": permission_blocked_sources,
|
|
859
|
+
}
|
|
860
|
+
)
|
|
774
861
|
verification["runtime_continuation_verified"] = runtime_verified
|
|
775
862
|
if not runtime_verified:
|
|
776
863
|
warnings.append(
|
|
@@ -931,7 +1018,9 @@ class TaskContextTools(ToolBase):
|
|
|
931
1018
|
baseline["workflow_log_visible"] = True
|
|
932
1019
|
baseline["workflow_log_count"] = len(log_items)
|
|
933
1020
|
baseline["workflow_log_digest"] = self._workflow_log_digest(log_items)
|
|
934
|
-
except QingflowApiError:
|
|
1021
|
+
except QingflowApiError as exc:
|
|
1022
|
+
if is_auth_like_error(exc):
|
|
1023
|
+
raise
|
|
935
1024
|
pass
|
|
936
1025
|
todo_items = self._safe_task_list_items(profile=profile, task_box="todo", app_key=app_key)
|
|
937
1026
|
baseline["downstream_todo_nodes"] = sorted(
|
|
@@ -1058,7 +1147,9 @@ class TaskContextTools(ToolBase):
|
|
|
1058
1147
|
page=1,
|
|
1059
1148
|
page_size=50,
|
|
1060
1149
|
)
|
|
1061
|
-
except QingflowApiError:
|
|
1150
|
+
except QingflowApiError as exc:
|
|
1151
|
+
if not _is_task_optional_read_error(exc):
|
|
1152
|
+
raise
|
|
1062
1153
|
return []
|
|
1063
1154
|
items = response.get("items") if isinstance(response, dict) else None
|
|
1064
1155
|
if not isinstance(items, list):
|
|
@@ -1152,21 +1243,35 @@ class TaskContextTools(ToolBase):
|
|
|
1152
1243
|
task_id_text = normalize_positive_id_text(task_id, field_name="task_id")
|
|
1153
1244
|
searched_task_boxes = ("todo", "initiated", "cc", "done")
|
|
1154
1245
|
incomplete_task_boxes: list[str] = []
|
|
1246
|
+
inaccessible_task_boxes: list[dict[str, Any]] = []
|
|
1155
1247
|
page_size = 100
|
|
1156
1248
|
for task_box in searched_task_boxes:
|
|
1157
1249
|
page = 1
|
|
1158
1250
|
page_amount: int | None = None
|
|
1159
1251
|
while True:
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1252
|
+
try:
|
|
1253
|
+
response = self._list_normalized_task_items(
|
|
1254
|
+
profile=profile,
|
|
1255
|
+
task_box=task_box,
|
|
1256
|
+
flow_status="all",
|
|
1257
|
+
app_key=None,
|
|
1258
|
+
workflow_node_id=None,
|
|
1259
|
+
query=None,
|
|
1260
|
+
page=page,
|
|
1261
|
+
page_size=page_size,
|
|
1262
|
+
)
|
|
1263
|
+
except QingflowApiError as exc:
|
|
1264
|
+
if not _is_optional_task_box_locator_error(exc):
|
|
1265
|
+
raise
|
|
1266
|
+
inaccessible_task_boxes.append(
|
|
1267
|
+
{
|
|
1268
|
+
"task_box": task_box,
|
|
1269
|
+
"backend_code": exc.backend_code,
|
|
1270
|
+
"http_status": exc.http_status,
|
|
1271
|
+
"request_id": exc.request_id,
|
|
1272
|
+
}
|
|
1273
|
+
)
|
|
1274
|
+
break
|
|
1170
1275
|
items = response.get("items") if isinstance(response.get("items"), list) else []
|
|
1171
1276
|
for item in items:
|
|
1172
1277
|
if not isinstance(item, dict) or not ids_equal(item.get("task_id"), task_id_text):
|
|
@@ -1200,6 +1305,18 @@ class TaskContextTools(ToolBase):
|
|
|
1200
1305
|
f"task_id={task_id_text} resolved to an incomplete task locator in task_box={searched}; please refresh the task list and retry"
|
|
1201
1306
|
)
|
|
1202
1307
|
)
|
|
1308
|
+
if inaccessible_task_boxes:
|
|
1309
|
+
searched = ", ".join(str(item.get("task_box")) for item in inaccessible_task_boxes)
|
|
1310
|
+
raise_tool_error(
|
|
1311
|
+
QingflowApiError.config_error(
|
|
1312
|
+
f"task_id={task_id_text} was not found in visible task boxes; some task boxes were not searchable in the current permission context: {searched}",
|
|
1313
|
+
details={
|
|
1314
|
+
"task_id": task_id_text,
|
|
1315
|
+
"searched_task_boxes": list(searched_task_boxes),
|
|
1316
|
+
"inaccessible_task_boxes": inaccessible_task_boxes,
|
|
1317
|
+
},
|
|
1318
|
+
)
|
|
1319
|
+
)
|
|
1203
1320
|
raise_tool_error(
|
|
1204
1321
|
QingflowApiError.config_error(
|
|
1205
1322
|
f"task_id={task_id_text} was not found in the current visible task boxes (todo, initiated, cc, done)"
|
|
@@ -1224,6 +1341,7 @@ class TaskContextTools(ToolBase):
|
|
|
1224
1341
|
resolved_app_key = str(locator["app_key"])
|
|
1225
1342
|
resolved_record_id = normalize_positive_id_int(locator["record_id"], field_name="record_id")
|
|
1226
1343
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1344
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
1227
1345
|
explicit_app_key = (app_key or "").strip()
|
|
1228
1346
|
if explicit_app_key and explicit_app_key != resolved_app_key:
|
|
1229
1347
|
raise_tool_error(
|
|
@@ -1248,8 +1366,10 @@ class TaskContextTools(ToolBase):
|
|
|
1248
1366
|
else:
|
|
1249
1367
|
resolved_record_id = normalize_positive_id_int(record_id, field_name="record_id")
|
|
1250
1368
|
resolved_workflow_node_id = int(workflow_node_id)
|
|
1369
|
+
resolved_task_box = "todo"
|
|
1251
1370
|
return {
|
|
1252
1371
|
"task_id": task_id_text,
|
|
1372
|
+
"task_box": resolved_task_box,
|
|
1253
1373
|
"app_key": resolved_app_key,
|
|
1254
1374
|
"record_id": resolved_record_id,
|
|
1255
1375
|
"record_id_text": stringify_backend_id(resolved_record_id),
|
|
@@ -1307,6 +1427,7 @@ class TaskContextTools(ToolBase):
|
|
|
1307
1427
|
resolved_record_id = int(locator["record_id"])
|
|
1308
1428
|
record_id_text = str(locator["record_id_text"] or "")
|
|
1309
1429
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1430
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
1310
1431
|
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
1311
1432
|
task_context = self._build_task_context(
|
|
1312
1433
|
profile=profile,
|
|
@@ -1314,6 +1435,7 @@ class TaskContextTools(ToolBase):
|
|
|
1314
1435
|
app_key=resolved_app_key,
|
|
1315
1436
|
record_id=resolved_record_id,
|
|
1316
1437
|
workflow_node_id=resolved_workflow_node_id,
|
|
1438
|
+
task_box=resolved_task_box,
|
|
1317
1439
|
include_candidates=False,
|
|
1318
1440
|
include_associated_reports=True,
|
|
1319
1441
|
current_uid=session_profile.uid,
|
|
@@ -1387,7 +1509,7 @@ class TaskContextTools(ToolBase):
|
|
|
1387
1509
|
|
|
1388
1510
|
chart_key = str(report_item.get("chart_key") or "")
|
|
1389
1511
|
source_type = report_item.get("source_type")
|
|
1390
|
-
if source_type
|
|
1512
|
+
if source_type in {"qingbi", "bi_qingflow", "bi_dataset"}:
|
|
1391
1513
|
qingbi_context = BackendRequestContext(
|
|
1392
1514
|
base_url=_qingbi_base_url(context.base_url),
|
|
1393
1515
|
token=context.token,
|
|
@@ -1396,20 +1518,44 @@ class TaskContextTools(ToolBase):
|
|
|
1396
1518
|
qf_version=context.qf_version,
|
|
1397
1519
|
qf_version_source=context.qf_version_source,
|
|
1398
1520
|
)
|
|
1399
|
-
|
|
1400
|
-
"
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
"
|
|
1411
|
-
|
|
1412
|
-
|
|
1521
|
+
chart_payload = {
|
|
1522
|
+
"asosChartId": report_id,
|
|
1523
|
+
"keyQueValues": association_query.get("keyQueValues") or [],
|
|
1524
|
+
}
|
|
1525
|
+
chart_params = {
|
|
1526
|
+
"qfUUID": uuid4().hex,
|
|
1527
|
+
"pageNum": page,
|
|
1528
|
+
"pageSize": page_size,
|
|
1529
|
+
}
|
|
1530
|
+
try:
|
|
1531
|
+
chart_result = self.backend.request(
|
|
1532
|
+
"POST",
|
|
1533
|
+
qingbi_context,
|
|
1534
|
+
f"/qingbi/charts/data/qflow/{chart_key}/detail",
|
|
1535
|
+
params=chart_params,
|
|
1536
|
+
json_body=chart_payload,
|
|
1537
|
+
)
|
|
1538
|
+
except QingflowApiError as error:
|
|
1539
|
+
if not _should_retry_asos_data(error):
|
|
1540
|
+
raise
|
|
1541
|
+
try:
|
|
1542
|
+
chart_result = self.backend.request(
|
|
1543
|
+
"POST",
|
|
1544
|
+
qingbi_context,
|
|
1545
|
+
f"/qingbi/charts/data/qflow/{chart_key}/asos",
|
|
1546
|
+
params=chart_params,
|
|
1547
|
+
json_body=chart_payload,
|
|
1548
|
+
)
|
|
1549
|
+
except QingflowApiError as fallback_error:
|
|
1550
|
+
if not _should_retry_asos_data(fallback_error):
|
|
1551
|
+
raise
|
|
1552
|
+
chart_result = self.backend.request(
|
|
1553
|
+
"POST",
|
|
1554
|
+
qingbi_context,
|
|
1555
|
+
f"/qingbi/charts/data/{chart_key}",
|
|
1556
|
+
params=chart_params,
|
|
1557
|
+
json_body=chart_payload,
|
|
1558
|
+
)
|
|
1413
1559
|
request_route = {
|
|
1414
1560
|
"base_url": qingbi_context.base_url,
|
|
1415
1561
|
"qf_version": qingbi_context.qf_version,
|
|
@@ -1509,6 +1655,7 @@ class TaskContextTools(ToolBase):
|
|
|
1509
1655
|
resolved_record_id = int(locator["record_id"])
|
|
1510
1656
|
record_id_text = str(locator["record_id_text"] or "")
|
|
1511
1657
|
resolved_workflow_node_id = int(locator["workflow_node_id"])
|
|
1658
|
+
resolved_task_box = str(locator.get("task_box") or "todo")
|
|
1512
1659
|
self._require_app_record_and_node(resolved_app_key, resolved_record_id, resolved_workflow_node_id)
|
|
1513
1660
|
task_context = self._build_task_context(
|
|
1514
1661
|
profile=profile,
|
|
@@ -1516,12 +1663,16 @@ class TaskContextTools(ToolBase):
|
|
|
1516
1663
|
app_key=resolved_app_key,
|
|
1517
1664
|
record_id=resolved_record_id,
|
|
1518
1665
|
workflow_node_id=resolved_workflow_node_id,
|
|
1666
|
+
task_box=resolved_task_box,
|
|
1519
1667
|
include_candidates=False,
|
|
1520
1668
|
include_associated_reports=False,
|
|
1521
1669
|
current_uid=session_profile.uid,
|
|
1522
1670
|
)
|
|
1523
1671
|
visibility = task_context.get("visibility") or {}
|
|
1524
|
-
|
|
1672
|
+
node = task_context.get("node") if isinstance(task_context.get("node"), dict) else {}
|
|
1673
|
+
raw_node = node.get("raw") if isinstance(node.get("raw"), dict) else {}
|
|
1674
|
+
audit_visibility_explicit = "auditRecordVisible" in raw_node
|
|
1675
|
+
if audit_visibility_explicit and not visibility.get("audit_record_visible"):
|
|
1525
1676
|
raise_tool_error(
|
|
1526
1677
|
QingflowApiError.config_error(
|
|
1527
1678
|
f"workflow logs are not visible for app_key='{resolved_app_key}' record_id={record_id_text} workflow_node_id={resolved_workflow_node_id}"
|
|
@@ -1535,7 +1686,7 @@ class TaskContextTools(ToolBase):
|
|
|
1535
1686
|
"key": resolved_app_key,
|
|
1536
1687
|
"rowRecordId": resolved_record_id,
|
|
1537
1688
|
"nodeId": resolved_workflow_node_id,
|
|
1538
|
-
"role":
|
|
1689
|
+
"role": _task_box_record_role(resolved_task_box),
|
|
1539
1690
|
"pageNum": 1,
|
|
1540
1691
|
"pageSize": 200,
|
|
1541
1692
|
},
|
|
@@ -1546,7 +1697,7 @@ class TaskContextTools(ToolBase):
|
|
|
1546
1697
|
"ws_id": session_profile.selected_ws_id,
|
|
1547
1698
|
"ok": True,
|
|
1548
1699
|
"request_route": self._request_route_payload(context),
|
|
1549
|
-
"warnings": [],
|
|
1700
|
+
"warnings": task_context.get("warnings") or [],
|
|
1550
1701
|
"output_profile": "normal",
|
|
1551
1702
|
"data": {
|
|
1552
1703
|
"selection": {
|
|
@@ -1555,7 +1706,9 @@ class TaskContextTools(ToolBase):
|
|
|
1555
1706
|
"workflow_node_id": resolved_workflow_node_id,
|
|
1556
1707
|
},
|
|
1557
1708
|
"visibility": {
|
|
1558
|
-
"audit_record_visible":
|
|
1709
|
+
"audit_record_visible": (
|
|
1710
|
+
visibility.get("audit_record_visible") if audit_visibility_explicit else None
|
|
1711
|
+
),
|
|
1559
1712
|
"qrobot_record_visible": visibility.get("qrobot_record_visible"),
|
|
1560
1713
|
},
|
|
1561
1714
|
"items": items,
|
|
@@ -1580,43 +1733,94 @@ class TaskContextTools(ToolBase):
|
|
|
1580
1733
|
include_candidates: bool,
|
|
1581
1734
|
include_associated_reports: bool,
|
|
1582
1735
|
current_uid: int | None = None,
|
|
1736
|
+
task_box: str = "todo",
|
|
1583
1737
|
) -> dict[str, Any]:
|
|
1584
1738
|
"""执行内部辅助逻辑。"""
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
node_info = self._select_task_node(audit_infos, workflow_node_id, app_key=app_key, record_id=record_id)
|
|
1592
|
-
detail = self.backend.request(
|
|
1593
|
-
"GET",
|
|
1594
|
-
context,
|
|
1595
|
-
f"/app/{app_key}/apply/{record_id}",
|
|
1596
|
-
params={"role": 3, "listType": 1, "auditNodeId": workflow_node_id},
|
|
1597
|
-
)
|
|
1598
|
-
app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
|
|
1599
|
-
associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
|
|
1600
|
-
associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": []}
|
|
1601
|
-
if include_associated_reports and associated_report_visible:
|
|
1602
|
-
asos_chart_list = self.backend.request(
|
|
1739
|
+
context_warnings: list[JSONObject] = []
|
|
1740
|
+
detail: dict[str, Any] | None = None
|
|
1741
|
+
list_type = _task_box_record_list_type(task_box)
|
|
1742
|
+
role = _task_box_record_role(task_box)
|
|
1743
|
+
try:
|
|
1744
|
+
audit_infos = self.backend.request(
|
|
1603
1745
|
"GET",
|
|
1604
1746
|
context,
|
|
1605
|
-
f"/app/{app_key}/
|
|
1606
|
-
params={"
|
|
1747
|
+
f"/app/{app_key}/apply/{record_id}/auditInfo",
|
|
1748
|
+
params={"type": list_type},
|
|
1607
1749
|
)
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
"
|
|
1616
|
-
"
|
|
1617
|
-
|
|
1618
|
-
|
|
1750
|
+
node_info = self._select_task_node(audit_infos, workflow_node_id, app_key=app_key, record_id=record_id)
|
|
1751
|
+
except QingflowApiError as exc:
|
|
1752
|
+
if not _is_optional_task_audit_info_error(exc):
|
|
1753
|
+
raise
|
|
1754
|
+
detail = self.backend.request(
|
|
1755
|
+
"GET",
|
|
1756
|
+
context,
|
|
1757
|
+
f"/app/{app_key}/apply/{record_id}",
|
|
1758
|
+
params={"role": role, "listType": list_type, "auditNodeId": workflow_node_id},
|
|
1759
|
+
)
|
|
1760
|
+
node_info = self._fallback_task_node_from_detail(
|
|
1761
|
+
detail,
|
|
1762
|
+
workflow_node_id=workflow_node_id,
|
|
1763
|
+
source_error=exc,
|
|
1764
|
+
)
|
|
1765
|
+
context_warnings.append(
|
|
1766
|
+
{
|
|
1767
|
+
"code": "TASK_AUDIT_INFO_UNAVAILABLE",
|
|
1768
|
+
"message": "Task context used the current task detail because auditInfo was unavailable in this permission context.",
|
|
1769
|
+
"backend_code": exc.backend_code,
|
|
1770
|
+
"request_id": exc.request_id,
|
|
1771
|
+
"http_status": exc.http_status,
|
|
1772
|
+
}
|
|
1773
|
+
)
|
|
1774
|
+
if detail is None:
|
|
1775
|
+
detail = self.backend.request(
|
|
1776
|
+
"GET",
|
|
1777
|
+
context,
|
|
1778
|
+
f"/app/{app_key}/apply/{record_id}",
|
|
1779
|
+
params={"role": role, "listType": list_type, "auditNodeId": workflow_node_id},
|
|
1780
|
+
)
|
|
1781
|
+
app_name = self._task_app_name(context=context, app_key=app_key, detail=detail, node_info=node_info)
|
|
1782
|
+
associated_report_visible = self._resolve_associated_report_visible(node_info, detail)
|
|
1783
|
+
associated_reports = {"visible": associated_report_visible, "loaded": False, "count": 0, "items": [], "warnings": []}
|
|
1784
|
+
if include_associated_reports and associated_report_visible:
|
|
1785
|
+
try:
|
|
1786
|
+
asos_chart_list = self.backend.request(
|
|
1787
|
+
"GET",
|
|
1788
|
+
context,
|
|
1789
|
+
f"/app/{app_key}/asosChart",
|
|
1790
|
+
params={"role": role, "auditNodeId": workflow_node_id, "beingDraft": False},
|
|
1791
|
+
)
|
|
1792
|
+
associated_items = [
|
|
1793
|
+
self._normalize_associated_report(item)
|
|
1794
|
+
for item in (asos_chart_list.get("asosCharts") or [])
|
|
1795
|
+
if isinstance(item, dict)
|
|
1796
|
+
]
|
|
1797
|
+
associated_reports = {
|
|
1798
|
+
"visible": True,
|
|
1799
|
+
"loaded": True,
|
|
1800
|
+
"count": len(associated_items),
|
|
1801
|
+
"items": associated_items,
|
|
1802
|
+
"warnings": [],
|
|
1803
|
+
}
|
|
1804
|
+
except QingflowApiError as exc:
|
|
1805
|
+
if not _is_task_optional_read_error(exc):
|
|
1806
|
+
raise
|
|
1807
|
+
associated_reports = {
|
|
1808
|
+
"visible": True,
|
|
1809
|
+
"loaded": False,
|
|
1810
|
+
"count": 0,
|
|
1811
|
+
"items": [],
|
|
1812
|
+
"warnings": [
|
|
1813
|
+
{
|
|
1814
|
+
"code": "TASK_ASSOCIATED_REPORTS_UNAVAILABLE",
|
|
1815
|
+
"message": "Associated reports are not readable in this permission context; task detail remains available.",
|
|
1816
|
+
"backend_code": exc.backend_code,
|
|
1817
|
+
"request_id": exc.request_id,
|
|
1818
|
+
"http_status": exc.http_status,
|
|
1819
|
+
}
|
|
1820
|
+
],
|
|
1821
|
+
}
|
|
1619
1822
|
rollback_items: list[dict[str, Any]] = []
|
|
1823
|
+
rollback_warnings: list[JSONObject] = []
|
|
1620
1824
|
transfer_items: list[dict[str, Any]] = []
|
|
1621
1825
|
transfer_warnings: list[JSONObject] = []
|
|
1622
1826
|
transfer_pagination: JSONObject = {
|
|
@@ -1628,13 +1832,26 @@ class TaskContextTools(ToolBase):
|
|
|
1628
1832
|
"truncated": False,
|
|
1629
1833
|
}
|
|
1630
1834
|
if include_candidates:
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1835
|
+
try:
|
|
1836
|
+
rollback_result = self.backend.request(
|
|
1837
|
+
"GET",
|
|
1838
|
+
context,
|
|
1839
|
+
f"/app/{app_key}/apply/{record_id}/revertNode",
|
|
1840
|
+
params={"auditNodeId": workflow_node_id},
|
|
1841
|
+
)
|
|
1842
|
+
rollback_items = self._rollback_candidate_items(rollback_result)
|
|
1843
|
+
except QingflowApiError as exc:
|
|
1844
|
+
if not _is_task_optional_read_error(exc):
|
|
1845
|
+
raise
|
|
1846
|
+
rollback_warnings.append(
|
|
1847
|
+
{
|
|
1848
|
+
"code": "TASK_ROLLBACK_CANDIDATES_UNAVAILABLE",
|
|
1849
|
+
"message": "Rollback candidates are not readable in this permission context; task detail remains available.",
|
|
1850
|
+
"backend_code": exc.backend_code,
|
|
1851
|
+
"request_id": exc.request_id,
|
|
1852
|
+
"http_status": exc.http_status,
|
|
1853
|
+
}
|
|
1854
|
+
)
|
|
1638
1855
|
transfer_items, transfer_warnings, transfer_pagination = self._transfer_candidate_items(
|
|
1639
1856
|
context,
|
|
1640
1857
|
app_key=app_key,
|
|
@@ -1657,6 +1874,7 @@ class TaskContextTools(ToolBase):
|
|
|
1657
1874
|
context,
|
|
1658
1875
|
app_key=app_key,
|
|
1659
1876
|
workflow_node_id=workflow_node_id,
|
|
1877
|
+
node_info=node_info,
|
|
1660
1878
|
)
|
|
1661
1879
|
capabilities = self._build_capabilities(
|
|
1662
1880
|
node_info,
|
|
@@ -1703,7 +1921,9 @@ class TaskContextTools(ToolBase):
|
|
|
1703
1921
|
"transfer_members": transfer_items,
|
|
1704
1922
|
"loaded": include_candidates,
|
|
1705
1923
|
"transfer_pagination": transfer_pagination,
|
|
1706
|
-
"
|
|
1924
|
+
"rollback_warnings": rollback_warnings,
|
|
1925
|
+
"transfer_warnings": transfer_warnings,
|
|
1926
|
+
"warnings": [*rollback_warnings, *transfer_warnings],
|
|
1707
1927
|
},
|
|
1708
1928
|
"workflow_log_summary": {
|
|
1709
1929
|
"visible": visibility["audit_record_visible"],
|
|
@@ -1712,6 +1932,7 @@ class TaskContextTools(ToolBase):
|
|
|
1712
1932
|
"qrobot_log_visible": visibility["qrobot_record_visible"],
|
|
1713
1933
|
},
|
|
1714
1934
|
"update_schema": update_schema,
|
|
1935
|
+
"warnings": context_warnings,
|
|
1715
1936
|
}
|
|
1716
1937
|
|
|
1717
1938
|
def _compact_task_get_context(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1781,12 +2002,14 @@ class TaskContextTools(ToolBase):
|
|
|
1781
2002
|
"loaded": bool(associated_reports.get("loaded")),
|
|
1782
2003
|
"count": len(associated_items),
|
|
1783
2004
|
"items": associated_items,
|
|
2005
|
+
"warnings": associated_reports.get("warnings") or [],
|
|
1784
2006
|
},
|
|
1785
2007
|
"rollback_candidates": {
|
|
1786
2008
|
"available": "rollback" in available_actions,
|
|
1787
2009
|
"loaded": bool(candidates.get("loaded")),
|
|
1788
2010
|
"count": len(rollback_items),
|
|
1789
2011
|
"items": rollback_items,
|
|
2012
|
+
"warnings": candidates.get("rollback_warnings") or [],
|
|
1790
2013
|
},
|
|
1791
2014
|
"transfer_candidates": {
|
|
1792
2015
|
"available": "transfer" in available_actions,
|
|
@@ -1794,7 +2017,7 @@ class TaskContextTools(ToolBase):
|
|
|
1794
2017
|
"count": len(transfer_items),
|
|
1795
2018
|
"items": transfer_items,
|
|
1796
2019
|
"pagination": transfer_pagination,
|
|
1797
|
-
"warnings": candidates.get("
|
|
2020
|
+
"warnings": candidates.get("transfer_warnings") or [],
|
|
1798
2021
|
},
|
|
1799
2022
|
},
|
|
1800
2023
|
}
|
|
@@ -1829,6 +2052,8 @@ class TaskContextTools(ToolBase):
|
|
|
1829
2052
|
metadata["blockers"] = blockers
|
|
1830
2053
|
if warnings:
|
|
1831
2054
|
metadata["warnings"] = warnings
|
|
2055
|
+
if update_schema.get("editable_question_ids_source"):
|
|
2056
|
+
metadata["editable_question_ids_source"] = update_schema.get("editable_question_ids_source")
|
|
1832
2057
|
return metadata
|
|
1833
2058
|
|
|
1834
2059
|
def _compact_initiator(self, payload: Any) -> dict[str, Any] | None:
|
|
@@ -1877,7 +2102,9 @@ class TaskContextTools(ToolBase):
|
|
|
1877
2102
|
) -> str | None:
|
|
1878
2103
|
try:
|
|
1879
2104
|
base_info = self.backend.request("GET", context, f"/app/{app_key}/baseInfo")
|
|
1880
|
-
except QingflowApiError:
|
|
2105
|
+
except QingflowApiError as exc:
|
|
2106
|
+
if not _is_task_optional_read_error(exc):
|
|
2107
|
+
raise
|
|
1881
2108
|
return None
|
|
1882
2109
|
if not isinstance(base_info, dict):
|
|
1883
2110
|
return None
|
|
@@ -1895,7 +2122,9 @@ class TaskContextTools(ToolBase):
|
|
|
1895
2122
|
) -> str | None:
|
|
1896
2123
|
try:
|
|
1897
2124
|
visible_apps = self.backend.request("GET", context, "/tag/apps")
|
|
1898
|
-
except QingflowApiError:
|
|
2125
|
+
except QingflowApiError as exc:
|
|
2126
|
+
if not _is_task_optional_read_error(exc):
|
|
2127
|
+
raise
|
|
1899
2128
|
return None
|
|
1900
2129
|
return self._find_task_app_name_in_visible_apps(visible_apps, app_key=app_key)
|
|
1901
2130
|
|
|
@@ -2095,6 +2324,54 @@ class TaskContextTools(ToolBase):
|
|
|
2095
2324
|
)
|
|
2096
2325
|
)
|
|
2097
2326
|
|
|
2327
|
+
def _fallback_task_node_from_detail(
|
|
2328
|
+
self,
|
|
2329
|
+
detail: dict[str, Any],
|
|
2330
|
+
*,
|
|
2331
|
+
workflow_node_id: int,
|
|
2332
|
+
source_error: QingflowApiError,
|
|
2333
|
+
) -> dict[str, Any]:
|
|
2334
|
+
"""Build a conservative task node snapshot from the readable task detail."""
|
|
2335
|
+
node_info: dict[str, Any] = {
|
|
2336
|
+
"auditNodeId": workflow_node_id,
|
|
2337
|
+
"nodeId": workflow_node_id,
|
|
2338
|
+
"auditNodeName": (
|
|
2339
|
+
detail.get("auditNodeName")
|
|
2340
|
+
or detail.get("nodeName")
|
|
2341
|
+
or detail.get("workflowNodeName")
|
|
2342
|
+
or detail.get("currentNodeName")
|
|
2343
|
+
),
|
|
2344
|
+
"_auditInfoUnavailable": True,
|
|
2345
|
+
"_auditInfoError": {
|
|
2346
|
+
"http_status": source_error.http_status,
|
|
2347
|
+
"backend_code": source_error.backend_code,
|
|
2348
|
+
"category": source_error.category,
|
|
2349
|
+
},
|
|
2350
|
+
}
|
|
2351
|
+
for key in (
|
|
2352
|
+
"canTransfer",
|
|
2353
|
+
"canRevert",
|
|
2354
|
+
"canUrge",
|
|
2355
|
+
"rejectBtnStatus",
|
|
2356
|
+
"canRevoke",
|
|
2357
|
+
"beingEndWorkflow",
|
|
2358
|
+
"beingCanApplyAgain",
|
|
2359
|
+
"beingSubmitCheck",
|
|
2360
|
+
"beingSubmitPreview",
|
|
2361
|
+
"feedbackRequiredOperationType",
|
|
2362
|
+
"queAuthSetting",
|
|
2363
|
+
"auditRecordVisible",
|
|
2364
|
+
"qrobotRecordBeingVisible",
|
|
2365
|
+
"beingWorkflowNodeFutureListVisible",
|
|
2366
|
+
"commentStatus",
|
|
2367
|
+
"asosChartVisible",
|
|
2368
|
+
):
|
|
2369
|
+
if key in detail:
|
|
2370
|
+
node_info[key] = detail[key]
|
|
2371
|
+
if "viewAsosChartVisible" in detail and "asosChartVisible" not in node_info:
|
|
2372
|
+
node_info["asosChartVisible"] = detail.get("viewAsosChartVisible")
|
|
2373
|
+
return node_info
|
|
2374
|
+
|
|
2098
2375
|
def _build_capabilities(
|
|
2099
2376
|
self,
|
|
2100
2377
|
node_info: dict[str, Any],
|
|
@@ -2159,35 +2436,18 @@ class TaskContextTools(ToolBase):
|
|
|
2159
2436
|
try:
|
|
2160
2437
|
app_schema = self._record_tools._get_form_schema(profile, context, app_key, force_refresh=False)
|
|
2161
2438
|
except QingflowApiError as error:
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
"app_key": app_key,
|
|
2175
|
-
"record_id": record_id_text,
|
|
2176
|
-
"workflow_node_id": workflow_node_id,
|
|
2177
|
-
},
|
|
2178
|
-
"transport_error": {
|
|
2179
|
-
"http_status": error.http_status,
|
|
2180
|
-
"backend_code": error.backend_code,
|
|
2181
|
-
"category": error.category,
|
|
2182
|
-
},
|
|
2183
|
-
}
|
|
2184
|
-
return {
|
|
2185
|
-
"public_schema": public_schema,
|
|
2186
|
-
"index": None,
|
|
2187
|
-
"editable_question_ids": [],
|
|
2188
|
-
"effective_editable_question_ids": [],
|
|
2189
|
-
"editable_question_ids_source": "schema_unavailable",
|
|
2190
|
-
}
|
|
2439
|
+
if not _is_task_optional_read_error(error):
|
|
2440
|
+
raise
|
|
2441
|
+
return self._build_task_update_schema_from_runtime_answers(
|
|
2442
|
+
profile=profile,
|
|
2443
|
+
context=context,
|
|
2444
|
+
app_key=app_key,
|
|
2445
|
+
record_id=record_id,
|
|
2446
|
+
workflow_node_id=workflow_node_id,
|
|
2447
|
+
node_info=node_info,
|
|
2448
|
+
current_answers=current_answers,
|
|
2449
|
+
source_error=error,
|
|
2450
|
+
)
|
|
2191
2451
|
|
|
2192
2452
|
question_relations = _collect_question_relations(app_schema)
|
|
2193
2453
|
linked_field_ids = _collect_linked_required_field_ids(question_relations)
|
|
@@ -2260,6 +2520,7 @@ class TaskContextTools(ToolBase):
|
|
|
2260
2520
|
"record_id": record_id_text,
|
|
2261
2521
|
"workflow_node_id": workflow_node_id,
|
|
2262
2522
|
},
|
|
2523
|
+
"editable_question_ids_source": source,
|
|
2263
2524
|
}
|
|
2264
2525
|
return {
|
|
2265
2526
|
"public_schema": public_schema,
|
|
@@ -2269,6 +2530,96 @@ class TaskContextTools(ToolBase):
|
|
|
2269
2530
|
"editable_question_ids_source": source,
|
|
2270
2531
|
}
|
|
2271
2532
|
|
|
2533
|
+
def _build_task_update_schema_from_runtime_answers(
|
|
2534
|
+
self,
|
|
2535
|
+
*,
|
|
2536
|
+
profile: str,
|
|
2537
|
+
context: BackendRequestContext,
|
|
2538
|
+
app_key: str,
|
|
2539
|
+
record_id: int,
|
|
2540
|
+
workflow_node_id: int,
|
|
2541
|
+
node_info: dict[str, Any],
|
|
2542
|
+
current_answers: Any,
|
|
2543
|
+
source_error: QingflowApiError,
|
|
2544
|
+
) -> dict[str, Any]:
|
|
2545
|
+
"""Build node-scoped edit schema from the task detail when app schema is unavailable."""
|
|
2546
|
+
record_id_text = stringify_backend_id(record_id)
|
|
2547
|
+
editable_question_ids, schema_warnings, source = self._resolve_task_editable_question_ids(
|
|
2548
|
+
context,
|
|
2549
|
+
app_key=app_key,
|
|
2550
|
+
workflow_node_id=workflow_node_id,
|
|
2551
|
+
node_info=node_info,
|
|
2552
|
+
)
|
|
2553
|
+
schema_warnings.insert(
|
|
2554
|
+
0,
|
|
2555
|
+
{
|
|
2556
|
+
"code": "TASK_UPDATE_SCHEMA_APP_SCHEMA_UNAVAILABLE",
|
|
2557
|
+
"message": "task update schema used current task answers because the app applicant schema was unavailable in this permission context.",
|
|
2558
|
+
"transport_error": {
|
|
2559
|
+
"http_status": source_error.http_status,
|
|
2560
|
+
"backend_code": source_error.backend_code,
|
|
2561
|
+
"category": source_error.category,
|
|
2562
|
+
},
|
|
2563
|
+
},
|
|
2564
|
+
)
|
|
2565
|
+
index = _build_answer_backed_field_index(
|
|
2566
|
+
current_answers,
|
|
2567
|
+
field_id_filter={str(que_id) for que_id in editable_question_ids if que_id > 0},
|
|
2568
|
+
)
|
|
2569
|
+
effective_editable_ids = set(editable_question_ids)
|
|
2570
|
+
writable_fields: list[JSONObject] = []
|
|
2571
|
+
for field in index.by_id.values():
|
|
2572
|
+
if field.que_type in LAYOUT_ONLY_QUE_TYPES or field.que_id not in effective_editable_ids:
|
|
2573
|
+
continue
|
|
2574
|
+
editable_field = _clone_form_field(field, readonly=False)
|
|
2575
|
+
write_hints = self._record_tools._schema_write_hints(editable_field)
|
|
2576
|
+
if not bool(write_hints.get("writable")):
|
|
2577
|
+
continue
|
|
2578
|
+
writable_field = self._record_tools._ready_schema_field_payload(
|
|
2579
|
+
profile,
|
|
2580
|
+
context,
|
|
2581
|
+
editable_field,
|
|
2582
|
+
ws_id=context.ws_id,
|
|
2583
|
+
required_override=False,
|
|
2584
|
+
linkage_payloads_by_field_id={},
|
|
2585
|
+
)
|
|
2586
|
+
writable_field.setdefault("field_id", editable_field.que_id)
|
|
2587
|
+
writable_fields.append(writable_field)
|
|
2588
|
+
|
|
2589
|
+
blockers: list[str] = []
|
|
2590
|
+
if not writable_fields:
|
|
2591
|
+
blockers.append("NO_TASK_EDITABLE_FIELDS")
|
|
2592
|
+
schema_warnings.append(
|
|
2593
|
+
{
|
|
2594
|
+
"code": "NO_TASK_EDITABLE_FIELDS",
|
|
2595
|
+
"message": "the current task node does not expose any writable fields in the current task detail.",
|
|
2596
|
+
}
|
|
2597
|
+
)
|
|
2598
|
+
public_schema: JSONObject = {
|
|
2599
|
+
"schema_scope": "task_update_ready",
|
|
2600
|
+
"writable_fields": writable_fields,
|
|
2601
|
+
"payload_template": {
|
|
2602
|
+
item["title"]: self._record_tools._ready_schema_template_value(item)
|
|
2603
|
+
for item in writable_fields
|
|
2604
|
+
if isinstance(item, dict) and item.get("title")
|
|
2605
|
+
},
|
|
2606
|
+
"blockers": blockers,
|
|
2607
|
+
"warnings": schema_warnings,
|
|
2608
|
+
"selection": {
|
|
2609
|
+
"app_key": app_key,
|
|
2610
|
+
"record_id": record_id_text,
|
|
2611
|
+
"workflow_node_id": workflow_node_id,
|
|
2612
|
+
},
|
|
2613
|
+
"editable_question_ids_source": f"{source}_runtime_answers",
|
|
2614
|
+
}
|
|
2615
|
+
return {
|
|
2616
|
+
"public_schema": public_schema,
|
|
2617
|
+
"index": index,
|
|
2618
|
+
"editable_question_ids": sorted(editable_question_ids),
|
|
2619
|
+
"effective_editable_question_ids": sorted(effective_editable_ids),
|
|
2620
|
+
"editable_question_ids_source": f"{source}_runtime_answers",
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2272
2623
|
def _augment_task_editable_field_index(
|
|
2273
2624
|
self,
|
|
2274
2625
|
*,
|
|
@@ -2328,12 +2679,15 @@ class TaskContextTools(ToolBase):
|
|
|
2328
2679
|
if question_ids:
|
|
2329
2680
|
return question_ids, warnings, "workflow_editable_que_ids"
|
|
2330
2681
|
except QingflowApiError as error:
|
|
2331
|
-
if
|
|
2682
|
+
if not _is_task_optional_read_error(error):
|
|
2332
2683
|
raise
|
|
2333
2684
|
warnings.append(
|
|
2334
2685
|
{
|
|
2335
2686
|
"code": "TASK_EDITABLE_IDS_FALLBACK",
|
|
2336
2687
|
"message": "editable question ids endpoint is unavailable in the current route; task update schema fell back to queAuthSetting and may be conservative.",
|
|
2688
|
+
"backend_code": error.backend_code,
|
|
2689
|
+
"http_status": error.http_status,
|
|
2690
|
+
"request_id": error.request_id,
|
|
2337
2691
|
}
|
|
2338
2692
|
)
|
|
2339
2693
|
fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
|
|
@@ -2345,6 +2699,7 @@ class TaskContextTools(ToolBase):
|
|
|
2345
2699
|
*,
|
|
2346
2700
|
app_key: str,
|
|
2347
2701
|
workflow_node_id: int,
|
|
2702
|
+
node_info: dict[str, Any],
|
|
2348
2703
|
) -> tuple[bool, list[JSONObject], str]:
|
|
2349
2704
|
"""执行内部辅助逻辑。"""
|
|
2350
2705
|
try:
|
|
@@ -2354,15 +2709,28 @@ class TaskContextTools(ToolBase):
|
|
|
2354
2709
|
f"/app/{app_key}/auditNode/{workflow_node_id}/editableQueIds",
|
|
2355
2710
|
)
|
|
2356
2711
|
except QingflowApiError as error:
|
|
2712
|
+
if not _is_task_optional_read_error(error):
|
|
2713
|
+
raise
|
|
2714
|
+
fallback_ids = self._editable_ids_from_que_auth_setting(node_info.get("queAuthSetting"))
|
|
2715
|
+
fallback_source = "que_auth_setting" if fallback_ids else "backend_editable_que_ids_unavailable"
|
|
2357
2716
|
warning: JSONObject = {
|
|
2358
|
-
"code": "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
|
|
2359
|
-
"message":
|
|
2717
|
+
"code": "TASK_SAVE_ONLY_EDITABLE_IDS_FALLBACK" if fallback_ids else "TASK_SAVE_ONLY_SIGNAL_UNAVAILABLE",
|
|
2718
|
+
"message": (
|
|
2719
|
+
"save_only availability used current task queAuthSetting because backend editableQueIds was unavailable in this permission context."
|
|
2720
|
+
if fallback_ids
|
|
2721
|
+
else "save_only is hidden because backend editableQueIds is unavailable and current task detail does not expose editable fields."
|
|
2722
|
+
),
|
|
2360
2723
|
}
|
|
2361
2724
|
if error.backend_code is not None:
|
|
2362
2725
|
warning["backend_code"] = error.backend_code
|
|
2363
2726
|
if error.http_status is not None:
|
|
2364
2727
|
warning["http_status"] = error.http_status
|
|
2365
|
-
|
|
2728
|
+
if error.request_id is not None:
|
|
2729
|
+
warning["request_id"] = error.request_id
|
|
2730
|
+
if fallback_ids:
|
|
2731
|
+
warning["field_ids"] = sorted(fallback_ids)
|
|
2732
|
+
return True, [warning], fallback_source
|
|
2733
|
+
return False, [warning], fallback_source
|
|
2366
2734
|
return bool(self._extract_question_ids(payload)), [], "workflow_editable_que_ids"
|
|
2367
2735
|
|
|
2368
2736
|
def _extract_question_ids(self, payload: Any) -> set[int]:
|
|
@@ -2655,12 +3023,33 @@ class TaskContextTools(ToolBase):
|
|
|
2655
3023
|
warnings: list[JSONObject] = []
|
|
2656
3024
|
|
|
2657
3025
|
while page_num <= max_pages:
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
3026
|
+
try:
|
|
3027
|
+
result = self.backend.request(
|
|
3028
|
+
"GET",
|
|
3029
|
+
context,
|
|
3030
|
+
f"/app/{app_key}/apply/{record_id}/transfer/member",
|
|
3031
|
+
params={"pageNum": page_num, "pageSize": page_size, "auditNodeId": workflow_node_id},
|
|
3032
|
+
)
|
|
3033
|
+
except QingflowApiError as exc:
|
|
3034
|
+
if not _is_task_optional_read_error(exc):
|
|
3035
|
+
raise
|
|
3036
|
+
warnings.append(
|
|
3037
|
+
{
|
|
3038
|
+
"code": "TASK_TRANSFER_CANDIDATES_UNAVAILABLE",
|
|
3039
|
+
"message": "Transfer candidates are not readable in this permission context; task detail remains available.",
|
|
3040
|
+
"backend_code": exc.backend_code,
|
|
3041
|
+
"request_id": exc.request_id,
|
|
3042
|
+
"http_status": exc.http_status,
|
|
3043
|
+
}
|
|
3044
|
+
)
|
|
3045
|
+
return items, warnings, {
|
|
3046
|
+
"loaded": False,
|
|
3047
|
+
"page_size": page_size,
|
|
3048
|
+
"fetched_pages": fetched_pages,
|
|
3049
|
+
"reported_total": reported_total,
|
|
3050
|
+
"page_amount": page_amount,
|
|
3051
|
+
"truncated": False,
|
|
3052
|
+
}
|
|
2664
3053
|
fetched_pages += 1
|
|
2665
3054
|
raw_items = _approval_page_items(result)
|
|
2666
3055
|
fetched_raw_count += len(raw_items)
|
|
@@ -2825,7 +3214,9 @@ class TaskContextTools(ToolBase):
|
|
|
2825
3214
|
context,
|
|
2826
3215
|
f"/chart/{chart_key}/auth",
|
|
2827
3216
|
)
|
|
2828
|
-
except QingflowApiError:
|
|
3217
|
+
except QingflowApiError as error:
|
|
3218
|
+
if not _is_task_optional_read_error(error):
|
|
3219
|
+
raise
|
|
2829
3220
|
return False
|
|
2830
3221
|
if not isinstance(auth, dict):
|
|
2831
3222
|
return False
|
|
@@ -3002,3 +3393,62 @@ class TaskContextTools(ToolBase):
|
|
|
3002
3393
|
"qf_version": context.qf_version,
|
|
3003
3394
|
"qf_version_source": context.qf_version_source or ("context" if context.qf_version else "unknown"),
|
|
3004
3395
|
}
|
|
3396
|
+
|
|
3397
|
+
|
|
3398
|
+
def _is_optional_task_audit_info_error(error: QingflowApiError) -> bool:
|
|
3399
|
+
"""Whether task auditInfo may be replaced by current task detail."""
|
|
3400
|
+
if is_auth_like_error(error):
|
|
3401
|
+
return False
|
|
3402
|
+
return _is_task_permission_context_error(error) or error.http_status == 404
|
|
3403
|
+
|
|
3404
|
+
|
|
3405
|
+
def _task_box_record_list_type(task_box: str | None) -> int:
|
|
3406
|
+
normalized = str(task_box or "todo").strip().lower()
|
|
3407
|
+
if normalized == "initiated":
|
|
3408
|
+
return 14
|
|
3409
|
+
if normalized == "done":
|
|
3410
|
+
return 2
|
|
3411
|
+
if normalized == "cc":
|
|
3412
|
+
return 12
|
|
3413
|
+
return 1
|
|
3414
|
+
|
|
3415
|
+
|
|
3416
|
+
def _task_box_record_role(task_box: str | None) -> int:
|
|
3417
|
+
normalized = str(task_box or "todo").strip().lower()
|
|
3418
|
+
if normalized == "initiated":
|
|
3419
|
+
return 2
|
|
3420
|
+
return 3
|
|
3421
|
+
|
|
3422
|
+
|
|
3423
|
+
def _is_optional_task_box_locator_error(error: QingflowApiError) -> bool:
|
|
3424
|
+
"""Whether one task box may be skipped while resolving a task_id."""
|
|
3425
|
+
if is_auth_like_error(error):
|
|
3426
|
+
return False
|
|
3427
|
+
return _is_task_permission_context_error(error) or error.http_status == 404
|
|
3428
|
+
|
|
3429
|
+
|
|
3430
|
+
def _is_task_optional_read_error(error: QingflowApiError) -> bool:
|
|
3431
|
+
if is_auth_like_error(error):
|
|
3432
|
+
return False
|
|
3433
|
+
backend_code = _task_backend_code(error)
|
|
3434
|
+
return backend_code in {40002, 40027, 404} or error.http_status == 404
|
|
3435
|
+
|
|
3436
|
+
|
|
3437
|
+
def _is_task_permission_context_error(error: QingflowApiError) -> bool:
|
|
3438
|
+
if is_auth_like_error(error):
|
|
3439
|
+
return False
|
|
3440
|
+
return _task_backend_code(error) in {40002, 40027}
|
|
3441
|
+
|
|
3442
|
+
|
|
3443
|
+
def _is_permission_context_error_payload(payload: dict[str, Any]) -> bool:
|
|
3444
|
+
if str(payload.get("category") or "").strip().lower() == "auth":
|
|
3445
|
+
return False
|
|
3446
|
+
if message_looks_like_invalid_token(payload.get("message")):
|
|
3447
|
+
return False
|
|
3448
|
+
if backend_code_value_int(payload.get("http_status")) == 401:
|
|
3449
|
+
return False
|
|
3450
|
+
return backend_code_value_int(payload.get("backend_code")) in {40002, 40027}
|
|
3451
|
+
|
|
3452
|
+
|
|
3453
|
+
def _task_backend_code(error: QingflowApiError) -> int | None:
|
|
3454
|
+
return backend_code_int(error)
|