@josephyan/qingflow-cli 0.2.0-beta.1014 → 0.2.0-beta.1015

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-cli@0.2.0-beta.1014
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.1015
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.1014 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.1015 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.1014",
3
+ "version": "0.2.0-beta.1015",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
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.0b1014"
7
+ version = "0.2.0b1015"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b1014"
8
+ _FALLBACK_VERSION = "0.2.0b1015"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -54,8 +54,8 @@ class QingflowApiError(Exception):
54
54
  )
55
55
 
56
56
  @classmethod
57
- def config_error(cls, message: str) -> "QingflowApiError":
58
- return cls(category="config", message=message)
57
+ def config_error(cls, message: str, *, details: JSONObject | None = None) -> "QingflowApiError":
58
+ return cls(category="config", message=message, details=details)
59
59
 
60
60
  @classmethod
61
61
  def not_supported(cls, message: str) -> "QingflowApiError":
@@ -19,6 +19,7 @@ from .base import ToolBase, tool_cn_name
19
19
  from .record_tools import (
20
20
  AccessibleViewRoute,
21
21
  DEFAULT_LIST_PAGE_SIZE,
22
+ FormField,
22
23
  LAYOUT_ONLY_QUE_TYPES,
23
24
  RecordTools,
24
25
  _normalize_public_column_selectors,
@@ -736,6 +737,11 @@ class ExportTools(ToolBase):
736
737
  filter_payload: JSONObject = {}
737
738
  if resolved_view.kind == "system" and resolved_view.list_type is not None:
738
739
  filter_payload["type"] = resolved_view.list_type
740
+ elif resolved_view.kind == "custom":
741
+ # Custom-view native export later flows through shared data-export code that
742
+ # expects a list semantics integer. Frontend export treats custom views as
743
+ # creator-all style export, so mirror LIST_CREATOR_ALL here.
744
+ filter_payload["type"] = DEFAULT_RECORD_LIST_TYPE
739
745
  if selected_record_ids:
740
746
  filter_payload["applyIds"] = selected_record_ids
741
747
  return {
@@ -763,18 +769,14 @@ class ExportTools(ToolBase):
763
769
  )
764
770
  index = browse_scope["index"]
765
771
  visible_question_ids = cast(set[int], browse_scope.get("visible_question_ids") or set())
766
- ordered_visible_fields = [
767
- field
768
- for field in self._record_tools._schema_fields_for_mode(
769
- profile,
770
- context,
771
- app_key,
772
- index,
773
- schema_mode="browse",
774
- resolved_view=resolved_view,
775
- )
776
- if field.que_id in visible_question_ids and field.que_type not in LAYOUT_ONLY_QUE_TYPES
777
- ]
772
+ ordered_visible_fields, warnings = self._resolve_exportable_fields(
773
+ profile=profile,
774
+ context=context,
775
+ app_key=app_key,
776
+ resolved_view=resolved_view,
777
+ index=index,
778
+ visible_question_ids=visible_question_ids,
779
+ )
778
780
  if not ordered_visible_fields:
779
781
  ordered_visible_fields = [
780
782
  field
@@ -805,7 +807,6 @@ class ExportTools(ToolBase):
805
807
  else:
806
808
  selected_fields = ordered_visible_fields
807
809
  question_export_config_list: list[JSONObject] = []
808
- warnings: list[JSONObject] = []
809
810
  for field in selected_fields:
810
811
  if field.que_type is None:
811
812
  warnings.append(
@@ -830,6 +831,79 @@ class ExportTools(ToolBase):
830
831
  )
831
832
  return {"questionExportConfigList": question_export_config_list}, warnings
832
833
 
834
+ def _resolve_exportable_fields(
835
+ self,
836
+ *,
837
+ profile: str,
838
+ context,
839
+ app_key: str,
840
+ resolved_view: AccessibleViewRoute,
841
+ index,
842
+ visible_question_ids: set[int],
843
+ ) -> tuple[list[FormField], list[JSONObject]]: # type: ignore[no-untyped-def]
844
+ warnings: list[JSONObject] = []
845
+ if resolved_view.kind == "custom" and resolved_view.view_selection is not None:
846
+ custom_fields = self._resolve_custom_view_exportable_fields(
847
+ profile=profile,
848
+ context=context,
849
+ app_key=app_key,
850
+ view_key=resolved_view.view_selection.view_key,
851
+ index=index,
852
+ visible_question_ids=visible_question_ids,
853
+ )
854
+ if custom_fields is not None:
855
+ return custom_fields, warnings
856
+ warnings.append(
857
+ {
858
+ "code": "EXPORT_VIEW_CONFIG_PARTIAL",
859
+ "message": "custom view export fields fell back to schema order because viewConfig could not provide exportable field entries",
860
+ }
861
+ )
862
+ ordered_visible_fields = [
863
+ field
864
+ for field in self._record_tools._schema_fields_for_mode(
865
+ profile,
866
+ context,
867
+ app_key,
868
+ index,
869
+ schema_mode="browse",
870
+ resolved_view=resolved_view,
871
+ )
872
+ if field.que_id in visible_question_ids and field.que_type not in LAYOUT_ONLY_QUE_TYPES
873
+ ]
874
+ return ordered_visible_fields, warnings
875
+
876
+ def _resolve_custom_view_exportable_fields(
877
+ self,
878
+ *,
879
+ profile: str,
880
+ context,
881
+ app_key: str,
882
+ view_key: str,
883
+ index,
884
+ visible_question_ids: set[int],
885
+ ) -> list[FormField] | None: # type: ignore[no-untyped-def]
886
+ view_config = self._record_tools._get_view_config(profile, context, view_key)
887
+ if not isinstance(view_config, dict):
888
+ return None
889
+ entries = _extract_export_view_question_entries(view_config.get("viewgraphQuestions"))
890
+ if not entries:
891
+ return []
892
+ ordered_fields: list[FormField] = []
893
+ seen: set[int] = set()
894
+ for entry in entries:
895
+ if not bool(entry.get("downloadable", True)):
896
+ continue
897
+ field_id = _coerce_int(entry.get("field_id"))
898
+ if field_id is None or field_id in seen:
899
+ continue
900
+ field = cast(FormField | None, cast(Any, index).by_id.get(str(field_id)))
901
+ if field is None or field.que_type in LAYOUT_ONLY_QUE_TYPES:
902
+ continue
903
+ ordered_fields.append(field)
904
+ seen.add(field_id)
905
+ return ordered_fields
906
+
833
907
  def _estimate_export_result_amount(
834
908
  self,
835
909
  context,
@@ -1241,6 +1315,58 @@ def _coerce_int(value: Any) -> int | None:
1241
1315
  return None
1242
1316
 
1243
1317
 
1318
+ def _coerce_positive_int(value: Any) -> int | None:
1319
+ parsed = _coerce_int(value)
1320
+ if parsed is None or parsed <= 0:
1321
+ return None
1322
+ return parsed
1323
+
1324
+
1325
+ def _extract_export_view_question_entries(questions: Any) -> list[JSONObject]:
1326
+ if not isinstance(questions, list):
1327
+ return []
1328
+ entries: list[JSONObject] = []
1329
+ fallback_order = 0
1330
+
1331
+ def walk(nodes: Any) -> None:
1332
+ nonlocal fallback_order
1333
+ if not isinstance(nodes, list):
1334
+ return
1335
+ for item in nodes:
1336
+ if not isinstance(item, dict):
1337
+ continue
1338
+ children: list[Any] = []
1339
+ for child_key in ("innerQues", "subQues", "innerQuestions", "subQuestions"):
1340
+ child_value = item.get(child_key)
1341
+ if isinstance(child_value, list) and child_value:
1342
+ children.extend(child_value)
1343
+ if children:
1344
+ walk(children)
1345
+ continue
1346
+ field_id = _coerce_int(item.get("queId"))
1347
+ if field_id is None:
1348
+ continue
1349
+ fallback_order += 1
1350
+ downloadable_raw = item.get("beingDownload")
1351
+ entries.append(
1352
+ {
1353
+ "field_id": field_id,
1354
+ "name": str(item.get("queTitle") or "").strip(),
1355
+ "display_order": _coerce_positive_int(item.get("displayOrdinal")) or fallback_order,
1356
+ "downloadable": bool(downloadable_raw) if downloadable_raw is not None else True,
1357
+ }
1358
+ )
1359
+
1360
+ walk(questions)
1361
+ return sorted(
1362
+ entries,
1363
+ key=lambda entry: (
1364
+ _coerce_positive_int(entry.get("display_order")) if _coerce_positive_int(entry.get("display_order")) is not None else 10**9,
1365
+ str(entry.get("name") or ""),
1366
+ ),
1367
+ )
1368
+
1369
+
1244
1370
  def _normalize_export_columns(columns: list[JSONObject | int]) -> list[int]:
1245
1371
  normalized: list[int] = []
1246
1372
  for field_id in _normalize_public_column_selectors(columns):
@@ -6890,6 +6890,8 @@ class RecordTools(ToolBase):
6890
6890
  isinstance(payload.get("viewgraphLimit"), list)
6891
6891
  or isinstance(payload.get("viewConfig"), dict)
6892
6892
  or isinstance(payload.get("viewgraphConfig"), dict)
6893
+ or isinstance(payload.get("viewgraphQuestions"), list)
6894
+ or isinstance(payload.get("viewgraphQueIds"), list)
6893
6895
  ):
6894
6896
  config = payload
6895
6897
  else: