@qingflow-tech/qingflow-cli 1.1.48 → 1.1.50

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.
Files changed (70) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +29 -13
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/skills/qingflow-cli/SKILL.md +91 -410
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  16. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +13 -5
  17. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +5 -3
  18. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +1 -1
  19. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +1 -1
  20. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1665 -1738
  21. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  22. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  26. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  27. package/skills/qingflow-cli/reference/data/README.md +32 -0
  28. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
  29. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  30. package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
  31. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  32. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  33. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  34. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  35. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
  36. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  37. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +48 -46
  38. package/skills/qingflow-common/SKILL.md +10 -17
  39. package/src/qingflow_mcp/__init__.py +1 -1
  40. package/src/qingflow_mcp/backend_client.py +142 -12
  41. package/src/qingflow_mcp/builder_facade/flow_validation.py +569 -0
  42. package/src/qingflow_mcp/builder_facade/service.py +762 -6
  43. package/src/qingflow_mcp/builder_facade/workflow_spec.py +155 -3
  44. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  45. package/src/qingflow_mcp/cli/commands/auth.py +1 -1
  46. package/src/qingflow_mcp/cli/commands/builder.py +57 -41
  47. package/src/qingflow_mcp/cli/commands/data.py +134 -0
  48. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  49. package/src/qingflow_mcp/cli/commands/task.py +10 -12
  50. package/src/qingflow_mcp/cli/commands/workspace.py +20 -7
  51. package/src/qingflow_mcp/cli/context.py +4 -1
  52. package/src/qingflow_mcp/cli/formatters.py +177 -21
  53. package/src/qingflow_mcp/cli/main.py +4 -1
  54. package/src/qingflow_mcp/cli/pagination.py +44 -0
  55. package/src/qingflow_mcp/config.py +60 -0
  56. package/src/qingflow_mcp/public_surface.py +9 -0
  57. package/src/qingflow_mcp/resource_uri.py +241 -0
  58. package/src/qingflow_mcp/response_trim.py +56 -5
  59. package/src/qingflow_mcp/server.py +2 -0
  60. package/src/qingflow_mcp/server_app_builder.py +57 -2
  61. package/src/qingflow_mcp/server_app_user.py +10 -3
  62. package/src/qingflow_mcp/tools/ai_builder_tools.py +374 -41
  63. package/src/qingflow_mcp/tools/app_tools.py +52 -13
  64. package/src/qingflow_mcp/tools/csv_export.py +168 -0
  65. package/src/qingflow_mcp/tools/data_tools.py +284 -0
  66. package/src/qingflow_mcp/tools/login_link_tools.py +34 -6
  67. package/src/qingflow_mcp/tools/record_tools.py +85 -61
  68. package/src/qingflow_mcp/tools/resource_read_tools.py +13 -0
  69. package/src/qingflow_mcp/tools/task_context_tools.py +21 -4
  70. package/src/qingflow_mcp/tools/workspace_tools.py +9 -12
@@ -1,6 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- import csv
4
3
  import html
5
4
  import mimetypes
6
5
  import json
@@ -34,6 +33,7 @@ from ..list_type_labels import (
34
33
  )
35
34
  from .base import ToolBase
36
35
  from .directory_tools import _directory_has_more, _directory_items
36
+ from .csv_export import CsvShardWriter
37
37
 
38
38
 
39
39
  DEFAULT_QUERY_PAGE_SIZE = 50
@@ -442,7 +442,7 @@ class RecordTools(ToolBase):
442
442
  description=(
443
443
  "Browse Qingflow records with a schema-first list DSL. "
444
444
  "Use record_browse_schema_get first, then pass field_id-only columns, where, and order_by clauses. "
445
- "This route returns up to 10 rows plus total counts for browse, sample inspection, and fuzzy record lookup; it is not for analysis."
445
+ "This route returns one page (10 rows by default) plus total counts for browse, sample inspection, and fuzzy record lookup; it is not for analysis."
446
446
  )
447
447
  )
448
448
  def record_list(
@@ -1525,6 +1525,7 @@ class RecordTools(ToolBase):
1525
1525
  )
1526
1526
  warnings: list[JSONObject] = []
1527
1527
  scope_source = "static_applicant_scope"
1528
+ backend_pagination: JSONObject = {}
1528
1529
  try:
1529
1530
  if runtime_lookup:
1530
1531
  state = self._build_candidate_lookup_state(
@@ -1535,7 +1536,10 @@ class RecordTools(ToolBase):
1535
1536
  workflow_node_id=workflow_node_id,
1536
1537
  fields=normalized_fields,
1537
1538
  )
1538
- items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
1539
+ items = self._resolve_member_candidates_backend(
1540
+ context, field, keyword=keyword, state=state,
1541
+ page_num=page_num, page_size=page_size, pagination=backend_pagination,
1542
+ )
1539
1543
  scope_source = "backend_runtime_scope"
1540
1544
  else:
1541
1545
  items: list[JSONObject] | None = None
@@ -1548,7 +1552,10 @@ class RecordTools(ToolBase):
1548
1552
  workflow_node_id=None,
1549
1553
  fields={},
1550
1554
  )
1551
- items = self._resolve_member_candidates_backend(context, field, keyword=keyword, state=state)
1555
+ items = self._resolve_member_candidates_backend(
1556
+ context, field, keyword=keyword, state=state,
1557
+ page_num=page_num, page_size=page_size, pagination=backend_pagination,
1558
+ )
1552
1559
  scope_source = "backend_applicant_scope"
1553
1560
  if items is None:
1554
1561
  items = self._resolve_member_candidates(context, field, keyword=keyword)
@@ -1581,7 +1588,7 @@ class RecordTools(ToolBase):
1581
1588
  total = len(items)
1582
1589
  start = (page_num - 1) * page_size
1583
1590
  end = start + page_size
1584
- page_items = items[start:end]
1591
+ page_items = items if backend_pagination else items[start:end]
1585
1592
  page_amount = (total + page_size - 1) // page_size if total else 0
1586
1593
  return {
1587
1594
  "profile": profile,
@@ -1598,6 +1605,7 @@ class RecordTools(ToolBase):
1598
1605
  "returned_items": len(page_items),
1599
1606
  "reported_total": total,
1600
1607
  "page_amount": page_amount,
1608
+ **backend_pagination,
1601
1609
  },
1602
1610
  "selection": {
1603
1611
  "app_key": app_key,
@@ -1668,6 +1676,7 @@ class RecordTools(ToolBase):
1668
1676
  )
1669
1677
  warnings: list[JSONObject] = []
1670
1678
  scope_source = "static_applicant_scope"
1679
+ backend_pagination: JSONObject = {}
1671
1680
  try:
1672
1681
  if runtime_lookup:
1673
1682
  state = self._build_candidate_lookup_state(
@@ -1678,7 +1687,10 @@ class RecordTools(ToolBase):
1678
1687
  workflow_node_id=workflow_node_id,
1679
1688
  fields=normalized_fields,
1680
1689
  )
1681
- items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
1690
+ items = self._resolve_department_candidates_backend(
1691
+ context, field, keyword=keyword, state=state,
1692
+ page_num=page_num, page_size=page_size, pagination=backend_pagination,
1693
+ )
1682
1694
  scope_source = "backend_runtime_scope"
1683
1695
  else:
1684
1696
  items: list[JSONObject] | None = None
@@ -1691,7 +1703,10 @@ class RecordTools(ToolBase):
1691
1703
  workflow_node_id=None,
1692
1704
  fields={},
1693
1705
  )
1694
- items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
1706
+ items = self._resolve_department_candidates_backend(
1707
+ context, field, keyword=keyword, state=state,
1708
+ page_num=page_num, page_size=page_size, pagination=backend_pagination,
1709
+ )
1695
1710
  scope_source = "backend_applicant_scope"
1696
1711
  if items is None:
1697
1712
  items = self._resolve_department_candidates(context, field, keyword=keyword)
@@ -1710,7 +1725,10 @@ class RecordTools(ToolBase):
1710
1725
  workflow_node_id=None,
1711
1726
  fields={},
1712
1727
  )
1713
- items = self._resolve_department_candidates_backend(context, field, keyword=keyword, state=state)
1728
+ items = self._resolve_department_candidates_backend(
1729
+ context, field, keyword=keyword, state=state,
1730
+ page_num=page_num, page_size=page_size, pagination=backend_pagination,
1731
+ )
1714
1732
  scope_source = "backend_applicant_scope"
1715
1733
  warnings.append(
1716
1734
  {
@@ -1741,7 +1759,7 @@ class RecordTools(ToolBase):
1741
1759
  total = len(items)
1742
1760
  start = (page_num - 1) * page_size
1743
1761
  end = start + page_size
1744
- page_items = items[start:end]
1762
+ page_items = items if backend_pagination else items[start:end]
1745
1763
  page_amount = (total + page_size - 1) // page_size if total else 0
1746
1764
  return {
1747
1765
  "profile": profile,
@@ -1758,6 +1776,7 @@ class RecordTools(ToolBase):
1758
1776
  "returned_items": len(page_items),
1759
1777
  "reported_total": total,
1760
1778
  "page_amount": page_amount,
1779
+ **backend_pagination,
1761
1780
  },
1762
1781
  "selection": {
1763
1782
  "app_key": app_key,
@@ -1888,7 +1907,7 @@ class RecordTools(ToolBase):
1888
1907
  query_fields: list[JSONObject | int] | None = None,
1889
1908
  where: list[JSONObject],
1890
1909
  order_by: list[JSONObject],
1891
- limit: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
1910
+ limit: int | None = None,
1892
1911
  page: int = 1,
1893
1912
  page_size: int = DEFAULT_RECORD_LIST_RETURN_LIMIT,
1894
1913
  view_id: str | None = None,
@@ -1905,12 +1924,14 @@ class RecordTools(ToolBase):
1905
1924
  normalized_columns = _normalize_public_column_selectors(columns)
1906
1925
  normalized_query = _normalize_optional_text(query)
1907
1926
  normalized_query_field_selectors = _normalize_public_query_field_selectors(query_fields or [])
1908
- if limit <= 0:
1927
+ if limit is not None and limit <= 0:
1909
1928
  raise_tool_error(QingflowApiError.config_error("limit must be positive"))
1910
1929
  if page <= 0:
1911
1930
  raise_tool_error(QingflowApiError.config_error("page must be positive"))
1912
1931
  if page_size <= 0:
1913
1932
  raise_tool_error(QingflowApiError.config_error("page_size must be positive"))
1933
+ if limit is not None:
1934
+ page_size = min(limit, page_size)
1914
1935
  if not (
1915
1936
  _normalize_optional_text(view_id)
1916
1937
  or list_type is not None
@@ -1989,7 +2010,7 @@ class RecordTools(ToolBase):
1989
2010
  search_que_ids=resolved_query_fields or None,
1990
2011
  match_rules=match_rules,
1991
2012
  sort_rules=sort_rules,
1992
- max_rows=min(limit, page_size),
2013
+ max_rows=page_size,
1993
2014
  selected_fields=selected_fields,
1994
2015
  output_profile="verbose" if normalized_output_profile in {"verbose", "normalized"} else DEFAULT_OUTPUT_PROFILE,
1995
2016
  )
@@ -2263,57 +2284,22 @@ class RecordTools(ToolBase):
2263
2284
  }
2264
2285
 
2265
2286
  run_dir = _record_access_run_dir()
2266
- run_dir.mkdir(parents=True, exist_ok=True)
2267
2287
  header = ["record_id"] + [str(field["column_name"]) for field in fields_payload]
2268
- files: list[JSONObject] = []
2269
- shard_part = 0
2270
- shard_rows = 0
2271
- row_count = 0
2272
- file_handle: Any | None = None
2273
- writer: Any | None = None
2274
-
2275
- def close_shard() -> None:
2276
- nonlocal file_handle, writer, shard_rows
2277
- if file_handle is None:
2278
- return
2279
- path = Path(file_handle.name)
2280
- file_handle.close()
2281
- files.append(
2282
- {
2283
- "local_path": str(path),
2284
- "row_count": shard_rows,
2285
- "part": len(files) + 1,
2286
- }
2287
- )
2288
- file_handle = None
2289
- writer = None
2290
- shard_rows = 0
2291
-
2292
- def ensure_writer() -> Any:
2293
- nonlocal file_handle, writer, shard_part
2294
- if writer is not None:
2295
- return writer
2296
- shard_part += 1
2297
- path = run_dir / f"records-{shard_part:04d}.csv"
2298
- file_handle = path.open("w", newline="", encoding="utf-8-sig")
2299
- writer = csv.writer(file_handle)
2300
- writer.writerow(header)
2301
- return writer
2288
+ shard_writer = CsvShardWriter(
2289
+ output_dir=run_dir,
2290
+ file_prefix="records",
2291
+ header=header,
2292
+ shard_rows=DEFAULT_RECORD_ACCESS_SHARD_ROWS,
2293
+ )
2302
2294
 
2303
2295
  def write_record(apply_id: int | None, answer_list: list[JSONValue]) -> None:
2304
- nonlocal row_count, shard_rows
2305
2296
  if apply_id is None:
2306
2297
  return
2307
- if shard_rows >= DEFAULT_RECORD_ACCESS_SHARD_ROWS:
2308
- close_shard()
2309
- csv_writer = ensure_writer()
2310
2298
  values: list[str] = [_public_record_id_text(apply_id) or ""]
2311
2299
  for field in selected_fields:
2312
2300
  answer = _find_answer_for_field(answer_list, field)
2313
2301
  values.append(_record_access_csv_cell(_normalize_answer_field_value_for_output(answer, field)))
2314
- csv_writer.writerow(values)
2315
- row_count += 1
2316
- shard_rows += 1
2302
+ shard_writer.write(values)
2317
2303
 
2318
2304
  current_page = 1
2319
2305
  has_more = False
@@ -2353,7 +2339,7 @@ class RecordTools(ToolBase):
2353
2339
  break
2354
2340
  page = fetch_page(current_page, BACKEND_RECORD_ACCESS_PAGE_SIZE)
2355
2341
  finally:
2356
- close_shard()
2342
+ shard_writer.close_shard()
2357
2343
 
2358
2344
  if stopped_reason == "TIME_BUDGET_EXCEEDED":
2359
2345
  warnings.append(_record_access_time_budget_warning(reported_total=reported_total, fetched_pages=fetched_pages))
@@ -2378,11 +2364,11 @@ class RecordTools(ToolBase):
2378
2364
  "view_id": view_route.view_id,
2379
2365
  "format": "csv",
2380
2366
  "local_dir": str(run_dir),
2381
- "row_count": row_count,
2367
+ "row_count": shard_writer.row_count,
2382
2368
  "complete": complete,
2383
2369
  "truncated": not complete,
2384
2370
  "safe_for_final_conclusion": safe_for_final_conclusion,
2385
- "files": files,
2371
+ "files": shard_writer.files,
2386
2372
  "fields": fields_payload,
2387
2373
  "warnings": warnings,
2388
2374
  "verification": verification_payload,
@@ -13314,6 +13300,9 @@ class RecordTools(ToolBase):
13314
13300
  *,
13315
13301
  keyword: str,
13316
13302
  state: LookupResolutionState,
13303
+ page_num: int = 1,
13304
+ page_size: int = LOOKUP_RELATION_FILTER_PAGE_SIZE,
13305
+ pagination: JSONObject | None = None,
13317
13306
  ) -> list[JSONObject]:
13318
13307
  """执行内部辅助逻辑。"""
13319
13308
  scope = field.member_select_scope if isinstance(field.member_select_scope, dict) else {}
@@ -13321,7 +13310,9 @@ class RecordTools(ToolBase):
13321
13310
  isinstance(scope.get("externalMemberList"), list) and scope.get("externalMemberList")
13322
13311
  or isinstance(scope.get("externalDepartList"), list) and scope.get("externalDepartList")
13323
13312
  )
13324
- payload = self._build_lookup_scope_payload(state, keyword=keyword, que_id=field.que_id)
13313
+ payload = self._build_lookup_scope_payload(
13314
+ state, keyword=keyword, que_id=field.que_id, page_num=page_num, page_size=page_size,
13315
+ )
13325
13316
  route = f"/data/que/{field.que_id}/external/memberSelectScope" if use_external else f"/data/que/{field.que_id}/memberSelectScope"
13326
13317
  try:
13327
13318
  result = self.backend.request("POST", context, route, json_body=payload)
@@ -13334,7 +13325,10 @@ class RecordTools(ToolBase):
13334
13325
  details={"field": _field_ref_payload(field), "received_value": keyword, "candidate_error": exc.to_dict()},
13335
13326
  )
13336
13327
  self._raise_candidate_lookup_failed(kind="member", field=field, value=keyword, error=exc)
13337
- return self._normalize_backend_member_candidates(result, external=use_external)
13328
+ items = self._normalize_backend_member_candidates(result, external=use_external)
13329
+ if pagination is not None:
13330
+ pagination.update(_candidate_page_metadata(result, page_num=page_num, page_size=page_size, returned_items=len(items)))
13331
+ return items
13338
13332
 
13339
13333
  def _member_confirmation_candidates(self, candidates: list[JSONObject]) -> list[JSONObject]:
13340
13334
  """执行内部辅助逻辑。"""
@@ -13603,16 +13597,24 @@ class RecordTools(ToolBase):
13603
13597
  *,
13604
13598
  keyword: str,
13605
13599
  state: LookupResolutionState,
13600
+ page_num: int = 1,
13601
+ page_size: int = LOOKUP_RELATION_FILTER_PAGE_SIZE,
13602
+ pagination: JSONObject | None = None,
13606
13603
  ) -> list[JSONObject]:
13607
13604
  """执行内部辅助逻辑。"""
13608
13605
  scope = field.dept_select_scope if isinstance(field.dept_select_scope, dict) else {}
13609
13606
  external = bool(
13610
13607
  isinstance(scope.get("externalDepartList"), list) and scope.get("externalDepartList")
13611
13608
  )
13612
- payload = self._build_lookup_scope_payload(state, keyword=keyword, que_id=field.que_id)
13609
+ payload = self._build_lookup_scope_payload(
13610
+ state, keyword=keyword, que_id=field.que_id, page_num=page_num, page_size=page_size,
13611
+ )
13613
13612
  payload["deptScope"] = "EXTERNAL" if external else "INTERNAL"
13614
13613
  result = self.backend.request("POST", context, "/data/deptSelectScope/search", json_body=payload)
13615
- return self._normalize_backend_department_candidates(result, external=external)
13614
+ items = self._normalize_backend_department_candidates(result, external=external)
13615
+ if pagination is not None:
13616
+ pagination.update(_candidate_page_metadata(result, page_num=page_num, page_size=page_size, returned_items=len(items)))
13617
+ return items
13616
13618
 
13617
13619
  def _department_confirmation_candidates(self, candidates: list[JSONObject]) -> list[JSONObject]:
13618
13620
  """执行内部辅助逻辑。"""
@@ -17881,6 +17883,28 @@ def _normalize_public_record_rows(rows: list[JSONValue]) -> list[JSONObject]:
17881
17883
  return normalized_rows
17882
17884
 
17883
17885
 
17886
+ def _candidate_page_metadata(
17887
+ payload: JSONValue, *, page_num: int, page_size: int, returned_items: int,
17888
+ ) -> JSONObject:
17889
+ page = payload if isinstance(payload, dict) else {}
17890
+ total = _coerce_count(page.get("total"))
17891
+ page_amount = _coerce_count(page.get("pageAmount"))
17892
+ # Some member-scope endpoints return rows without a total. A full page is not a total.
17893
+ if total is None and returned_items < page_size and (page_num == 1 or returned_items > 0):
17894
+ total = (page_num - 1) * page_size + returned_items
17895
+ if page_amount is None and total is not None:
17896
+ page_amount = (total + page_size - 1) // page_size
17897
+ has_more = page_num < page_amount if page_amount is not None else returned_items >= page_size
17898
+ return {
17899
+ "page": page_num,
17900
+ "page_size": page_size,
17901
+ "returned_items": returned_items,
17902
+ "reported_total": total,
17903
+ "page_amount": page_amount,
17904
+ "has_more": has_more,
17905
+ }
17906
+
17907
+
17884
17908
  def _build_record_list_lookup_payload(
17885
17909
  *,
17886
17910
  query: str | None,
@@ -6,6 +6,7 @@ from typing import Any
6
6
  from ..errors import QingflowApiError, backend_code_int, is_auth_like_error, raise_tool_error
7
7
  from ..json_types import JSONObject
8
8
  from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
9
+ from ..resource_uri import try_build_resource_uri
9
10
  from ..solution.compiler.icon_utils import workspace_icon_config
10
11
  from .app_tools import _analysis_supported_for_view_type
11
12
  from .base import ToolBase
@@ -227,6 +228,17 @@ class ResourceReadTools(ToolBase):
227
228
  "message": f"view_get supports exporting `{view_id}`, but the export call still needs an explicit `app_key` from the parent app context.",
228
229
  }
229
230
  )
231
+ uri = None
232
+ if resolved_app_key:
233
+ uri, uri_warning = try_build_resource_uri(
234
+ ws_id=session_profile.selected_ws_id,
235
+ resource_type="view",
236
+ app_key=resolved_app_key,
237
+ view_kind="custom",
238
+ view_key=view_key,
239
+ )
240
+ if uri_warning:
241
+ warnings.append(uri_warning)
230
242
  return {
231
243
  "profile": profile,
232
244
  "ws_id": session_profile.selected_ws_id,
@@ -249,6 +261,7 @@ class ResourceReadTools(ToolBase):
249
261
  ],
250
262
  "analysis_supported": _analysis_supported_for_view_type(raw_view_type or None),
251
263
  "export_capability": export_capability,
264
+ **({"uri": uri} if uri else {}),
252
265
  },
253
266
  }
254
267
 
@@ -201,7 +201,15 @@ class TaskContextTools(ToolBase):
201
201
  items = response["items"] if isinstance(response.get("items"), list) else []
202
202
  page_amount = response.get("page_amount")
203
203
  reported_total = response.get("reported_total")
204
- if query and not items:
204
+ search_has_no_matches = not items and not (_coerce_count(reported_total) or 0)
205
+ if query and search_has_no_matches and page > 1:
206
+ first_page = self._list_normalized_task_items(
207
+ profile=profile, task_box=task_box, flow_status=flow_status,
208
+ app_key=app_key, workflow_node_id=workflow_node_id,
209
+ query=query, page=1, page_size=page_size,
210
+ )
211
+ search_has_no_matches = not first_page["items"] and not (_coerce_count(first_page.get("reported_total")) or 0)
212
+ if query and search_has_no_matches:
205
213
  fallback = self._task_list_local_query_fallback(
206
214
  profile=profile,
207
215
  task_box=task_box,
@@ -1238,6 +1246,7 @@ class TaskContextTools(ToolBase):
1238
1246
  scan_page = 1
1239
1247
  page_amount: int | None = None
1240
1248
  matched_items: list[dict[str, Any]] = []
1249
+ scanned_items = 0
1241
1250
  while True:
1242
1251
  response = self._list_normalized_task_items(
1243
1252
  profile=profile,
@@ -1250,14 +1259,22 @@ class TaskContextTools(ToolBase):
1250
1259
  page_size=scan_page_size,
1251
1260
  )
1252
1261
  normalized_items = response.get("items") if isinstance(response.get("items"), list) else []
1262
+ scanned_items += len(normalized_items)
1253
1263
  matched_items.extend(item for item in normalized_items if self._task_item_matches_query(item, query))
1254
1264
  if page_amount is None:
1255
1265
  coerced_page_amount = _coerce_count(response.get("page_amount"))
1256
1266
  if coerced_page_amount is not None and coerced_page_amount > 0:
1257
1267
  page_amount = coerced_page_amount
1258
- if page_amount is not None and scan_page >= page_amount:
1268
+ if not normalized_items:
1259
1269
  break
1260
- if not normalized_items or len(normalized_items) < scan_page_size:
1270
+ reported_total = _coerce_count(response.get("reported_total"))
1271
+ if page_amount is not None:
1272
+ if scan_page >= page_amount:
1273
+ break
1274
+ elif reported_total is not None:
1275
+ if scanned_items >= reported_total:
1276
+ break
1277
+ elif len(normalized_items) < scan_page_size:
1261
1278
  break
1262
1279
  scan_page += 1
1263
1280
  if not matched_items:
@@ -1527,7 +1544,7 @@ class TaskContextTools(ToolBase):
1527
1544
  if report_item.get("graph_type") == "view":
1528
1545
  viewgraph_key = str(report_item.get("chart_key") or "")
1529
1546
  body = {
1530
- "filter": {},
1547
+ "filter": {"pageNum": page, "pageSize": page_size},
1531
1548
  "viewgraphKey": viewgraph_key,
1532
1549
  "equipmentType": 0,
1533
1550
  "associationQuery": association_query,
@@ -94,8 +94,13 @@ class WorkspaceTools(ToolBase):
94
94
  normalized = result if isinstance(result, dict) else {"list": result if isinstance(result, list) else []}
95
95
  workspace_list = normalized.get("list") if isinstance(normalized.get("list"), list) else []
96
96
  selected_ws_id = _.selected_ws_id if _ is not None else None
97
+ selected_in_page = bool(
98
+ selected_ws_id
99
+ and any(isinstance(item, dict) and item.get("wsId") == selected_ws_id for item in workspace_list)
100
+ )
97
101
  warnings: list[dict[str, Any]] = []
98
- if selected_ws_id and not any(isinstance(item, dict) and item.get("wsId") == selected_ws_id for item in workspace_list):
102
+ selected_workspace = None
103
+ if page_num == 1 and selected_ws_id and not selected_in_page:
99
104
  try:
100
105
  selected_workspace = self.backend.request("GET", context, f"/user/workspace/{selected_ws_id}")
101
106
  except QingflowApiError as exc:
@@ -106,23 +111,15 @@ class WorkspaceTools(ToolBase):
106
111
  "wsId": selected_ws_id,
107
112
  "workspaceName": _.selected_ws_name if _ is not None else None,
108
113
  }
109
- if isinstance(selected_workspace, dict):
110
- workspace_list.append(selected_workspace)
111
- normalized["list"] = workspace_list
112
- total = normalized.get("total")
113
- if isinstance(total, int):
114
- normalized["total"] = total + 1
115
114
  return {
116
115
  "profile": profile,
117
116
  "include_external": include_external,
118
117
  "page": normalized,
118
+ "selected_workspace": selected_workspace,
119
119
  "warnings": warnings,
120
120
  "verification": {
121
- "selected_workspace_detail_verified": not warnings,
122
- "selected_workspace_included": bool(
123
- selected_ws_id
124
- and any(isinstance(item, dict) and item.get("wsId") == selected_ws_id for item in workspace_list)
125
- ),
121
+ "selected_workspace_detail_verified": not warnings and (selected_in_page or isinstance(selected_workspace, dict)),
122
+ "selected_workspace_included": selected_in_page,
126
123
  },
127
124
  }
128
125