@josephyan/qingflow-app-builder-mcp 0.2.0-beta.1011 → 0.2.0-beta.1012

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-builder-mcp@0.2.0-beta.1011
6
+ npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1012
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1011 qingflow-app-builder-mcp
12
+ npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1012 qingflow-app-builder-mcp
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-app-builder-mcp",
3
- "version": "0.2.0-beta.1011",
3
+ "version": "0.2.0-beta.1012",
4
4
  "description": "Builder MCP for Qingflow app/package/system design and staged solution 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.0b1011"
7
+ version = "0.2.0b1012"
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.0b1011"
8
+ _FALLBACK_VERSION = "0.2.0b1012"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -15,6 +15,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
15
15
  start.add_argument("--view-id", default="system:all")
16
16
  start.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
17
17
  start.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
18
+ start.add_argument("--where-file", help="JSON/YAML list,内容与 record list 的 where DSL 一致;内部先查命中 record_id 再走原生导出")
18
19
  start.add_argument("--record-id", dest="record_ids", action="append", default=[], help="只导出这些 record_id;不传时导出当前视图全部数据")
19
20
  start.add_argument("--record-ids-file", help="JSON/YAML list,内容与 --record-id 语义一致")
20
21
  start.add_argument("--include-workflow-log", action=argparse.BooleanOptionalAction, default=False, help="是否同时导出流程日志")
@@ -34,6 +35,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
34
35
  direct.add_argument("--view-id", default="system:all")
35
36
  direct.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
36
37
  direct.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
38
+ direct.add_argument("--where-file", help="JSON/YAML list,内容与 record list 的 where DSL 一致;内部先查命中 record_id 再走原生导出")
37
39
  direct.add_argument("--record-id", dest="record_ids", action="append", default=[], help="只导出这些 record_id;不传时导出当前视图全部数据")
38
40
  direct.add_argument("--record-ids-file", help="JSON/YAML list,内容与 --record-id 语义一致")
39
41
  direct.add_argument("--include-workflow-log", action=argparse.BooleanOptionalAction, default=False, help="是否同时导出流程日志")
@@ -56,12 +58,17 @@ def _record_ids(args: argparse.Namespace) -> list[str | int]:
56
58
  return record_ids
57
59
 
58
60
 
61
+ def _where(args: argparse.Namespace) -> list[dict]:
62
+ return load_list_arg(args.where_file, option_name="--where-file") if args.where_file else []
63
+
64
+
59
65
  def _handle_start(args: argparse.Namespace, context: CliContext) -> dict:
60
66
  return context.exports.record_export_start(
61
67
  profile=args.profile,
62
68
  app_key=args.app_key,
63
69
  view_id=args.view_id,
64
70
  columns=_columns(args),
71
+ where=_where(args),
65
72
  record_ids=_record_ids(args),
66
73
  include_workflow_log=args.include_workflow_log,
67
74
  )
@@ -88,6 +95,7 @@ def _handle_direct(args: argparse.Namespace, context: CliContext) -> dict:
88
95
  app_key=args.app_key,
89
96
  view_id=args.view_id,
90
97
  columns=_columns(args),
98
+ where=_where(args),
91
99
  record_ids=_record_ids(args),
92
100
  include_workflow_log=args.include_workflow_log,
93
101
  download_to_path=args.download_to_path,
@@ -158,6 +158,10 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
158
158
  - `record_export_start` / `record_export_direct` support frontend-like row selection:
159
159
  - omit `record_ids` to export all rows in the selected view
160
160
  - pass `record_ids` to export selected rows only
161
+ - `record_export_start` / `record_export_direct` also support internal query selection:
162
+ - pass `where` to resolve matching `record_id` values first
163
+ - then run native export as selected rows
164
+ - `where` and `record_ids` are mutually exclusive
161
165
  - `record_export_start` / `record_export_direct` also support frontend-like column selection:
162
166
  - omit `columns` to export all current-view fields
163
167
  - pass `columns` to export only selected fields, preserving the provided order
@@ -153,6 +153,10 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
153
153
  - `record_export_start` / `record_export_direct` support frontend-like row selection:
154
154
  - omit `record_ids` to export all rows in the selected view
155
155
  - pass `record_ids` to export selected rows only
156
+ - `record_export_start` / `record_export_direct` also support internal query selection:
157
+ - pass `where` to resolve matching `record_id` values first
158
+ - then run native export as selected rows
159
+ - `where` and `record_ids` are mutually exclusive
156
160
  - `record_export_start` / `record_export_direct` also support frontend-like column selection:
157
161
  - omit `columns` to export all current-view fields
158
162
  - pass `columns` to export only selected fields, preserving the provided order
@@ -241,6 +245,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
241
245
  app_key: str = "",
242
246
  view_id: str = "system:all",
243
247
  columns: list[dict | int] | None = None,
248
+ where: list[dict] | None = None,
244
249
  record_ids: list[str | int] | None = None,
245
250
  include_workflow_log: bool = False,
246
251
  ) -> dict:
@@ -249,6 +254,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
249
254
  app_key=app_key,
250
255
  view_id=view_id,
251
256
  columns=columns or [],
257
+ where=where or [],
252
258
  record_ids=record_ids or [],
253
259
  include_workflow_log=include_workflow_log,
254
260
  )
@@ -278,6 +284,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
278
284
  app_key: str = "",
279
285
  view_id: str = "system:all",
280
286
  columns: list[dict | int] | None = None,
287
+ where: list[dict] | None = None,
281
288
  record_ids: list[str | int] | None = None,
282
289
  include_workflow_log: bool = False,
283
290
  download_to_path: str | None = None,
@@ -288,6 +295,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
288
295
  app_key=app_key,
289
296
  view_id=view_id,
290
297
  columns=columns or [],
298
+ where=where or [],
291
299
  record_ids=record_ids or [],
292
300
  include_workflow_log=include_workflow_log,
293
301
  download_to_path=download_to_path,
@@ -18,6 +18,7 @@ from ..json_types import JSONObject
18
18
  from .base import ToolBase, tool_cn_name
19
19
  from .record_tools import (
20
20
  AccessibleViewRoute,
21
+ DEFAULT_LIST_PAGE_SIZE,
21
22
  LAYOUT_ONLY_QUE_TYPES,
22
23
  RecordTools,
23
24
  _normalize_public_column_selectors,
@@ -58,6 +59,7 @@ class ExportTools(ToolBase):
58
59
  app_key: str = "",
59
60
  view_id: str = "system:all",
60
61
  columns: list[JSONObject | int] | None = None,
62
+ where: list[JSONObject] | None = None,
61
63
  record_ids: list[str | int] | None = None,
62
64
  include_workflow_log: bool = False,
63
65
  ) -> dict[str, Any]:
@@ -66,6 +68,7 @@ class ExportTools(ToolBase):
66
68
  app_key=app_key,
67
69
  view_id=view_id,
68
70
  columns=columns or [],
71
+ where=where or [],
69
72
  record_ids=record_ids or [],
70
73
  include_workflow_log=include_workflow_log,
71
74
  )
@@ -98,6 +101,7 @@ class ExportTools(ToolBase):
98
101
  app_key: str = "",
99
102
  view_id: str = "system:all",
100
103
  columns: list[JSONObject | int] | None = None,
104
+ where: list[JSONObject] | None = None,
101
105
  record_ids: list[str | int] | None = None,
102
106
  include_workflow_log: bool = False,
103
107
  download_to_path: str | None = None,
@@ -108,6 +112,7 @@ class ExportTools(ToolBase):
108
112
  app_key=app_key,
109
113
  view_id=view_id,
110
114
  columns=columns or [],
115
+ where=where or [],
111
116
  record_ids=record_ids or [],
112
117
  include_workflow_log=include_workflow_log,
113
118
  download_to_path=download_to_path,
@@ -122,12 +127,14 @@ class ExportTools(ToolBase):
122
127
  app_key: str,
123
128
  view_id: str = "system:all",
124
129
  columns: list[JSONObject | int] | None = None,
130
+ where: list[JSONObject] | None = None,
125
131
  record_ids: list[str | int] | None = None,
126
132
  include_workflow_log: bool = False,
127
133
  ) -> dict[str, Any]:
128
134
  normalized_app_key = str(app_key or "").strip()
129
135
  normalized_view_id = str(view_id or "").strip() or "system:all"
130
136
  normalized_columns = _normalize_export_columns(columns or [])
137
+ normalized_where = self._record_tools._normalize_record_list_where(where or [])
131
138
  normalized_record_ids = _normalize_export_record_ids(record_ids or [])
132
139
  if not normalized_app_key:
133
140
  return self._failed_export_result(
@@ -154,11 +161,20 @@ class ExportTools(ToolBase):
154
161
  resolved_view=resolved_view,
155
162
  column_selectors=normalized_columns,
156
163
  )
164
+ effective_record_ids, row_scope = self._resolve_export_record_scope(
165
+ profile=profile,
166
+ context=context,
167
+ app_key=normalized_app_key,
168
+ resolved_view=resolved_view,
169
+ selected_record_ids=normalized_record_ids,
170
+ where_filters=normalized_where,
171
+ select_columns=cast(list[JSONObject], export_config.get("questionExportConfigList") or []),
172
+ )
157
173
  result_amount = self._estimate_export_result_amount(
158
174
  context,
159
175
  app_key=normalized_app_key,
160
176
  resolved_view=resolved_view,
161
- selected_record_ids=normalized_record_ids,
177
+ selected_record_ids=effective_record_ids,
162
178
  )
163
179
  self._validate_export_limits(
164
180
  result_amount=result_amount,
@@ -167,7 +183,7 @@ class ExportTools(ToolBase):
167
183
  )
168
184
  filter_bean = self._build_export_filter_bean(
169
185
  resolved_view,
170
- selected_record_ids=normalized_record_ids,
186
+ selected_record_ids=effective_record_ids,
171
187
  include_workflow_log=include_workflow_log,
172
188
  )
173
189
  started_at = _utc_now().replace(microsecond=0).isoformat()
@@ -180,7 +196,6 @@ class ExportTools(ToolBase):
180
196
  export_config=export_config,
181
197
  result_amount=result_amount,
182
198
  )
183
- row_scope = "selected" if normalized_record_ids else "all"
184
199
  column_scope = "selected" if normalized_columns else "all"
185
200
  export_handle = uuid4().hex
186
201
  self._job_store.put(
@@ -194,7 +209,7 @@ class ExportTools(ToolBase):
194
209
  "started_at": started_at,
195
210
  "uid": session_profile.uid,
196
211
  "row_scope": row_scope,
197
- "selected_record_count": len(normalized_record_ids),
212
+ "selected_record_count": len(effective_record_ids),
198
213
  "field_scope": column_scope,
199
214
  "selected_field_count": len(cast(list[JSONObject], export_config.get("questionExportConfigList") or [])),
200
215
  "include_workflow_log": bool(include_workflow_log),
@@ -212,7 +227,7 @@ class ExportTools(ToolBase):
212
227
  "view_id": resolved_view.view_id,
213
228
  "export_handle": export_handle,
214
229
  "row_scope": row_scope,
215
- "selected_record_count": len(normalized_record_ids),
230
+ "selected_record_count": len(effective_record_ids),
216
231
  "field_scope": column_scope,
217
232
  "selected_field_count": len(cast(list[JSONObject], export_config.get("questionExportConfigList") or [])),
218
233
  "include_workflow_log": bool(include_workflow_log),
@@ -223,7 +238,8 @@ class ExportTools(ToolBase):
223
238
  "verification": {
224
239
  "export_acknowledged": bool(socket_result.get("backend_export_id")),
225
240
  "view_route_supported": True,
226
- "row_selection_applied": bool(normalized_record_ids),
241
+ "row_selection_applied": bool(effective_record_ids),
242
+ "query_filter_applied": bool(normalized_where),
227
243
  "field_selection_applied": bool(normalized_columns),
228
244
  },
229
245
  "request_route": self.backend.describe_route(context),
@@ -407,6 +423,7 @@ class ExportTools(ToolBase):
407
423
  app_key: str,
408
424
  view_id: str = "system:all",
409
425
  columns: list[JSONObject | int] | None = None,
426
+ where: list[JSONObject] | None = None,
410
427
  record_ids: list[str | int] | None = None,
411
428
  include_workflow_log: bool = False,
412
429
  download_to_path: str | None = None,
@@ -428,6 +445,7 @@ class ExportTools(ToolBase):
428
445
  app_key=normalized_app_key,
429
446
  view_id=normalized_view_id,
430
447
  columns=columns or [],
448
+ where=where or [],
431
449
  record_ids=record_ids or [],
432
450
  include_workflow_log=include_workflow_log,
433
451
  )
@@ -552,6 +570,141 @@ class ExportTools(ToolBase):
552
570
  extra={"app_key": normalized_app_key, "view_id": normalized_view_id},
553
571
  )
554
572
 
573
+ def _resolve_export_record_scope(
574
+ self,
575
+ *,
576
+ profile: str,
577
+ context,
578
+ app_key: str,
579
+ resolved_view: AccessibleViewRoute,
580
+ selected_record_ids: list[int],
581
+ where_filters: list[JSONObject],
582
+ select_columns: list[JSONObject],
583
+ ) -> tuple[list[int], str]:
584
+ if selected_record_ids and where_filters:
585
+ raise QingflowApiError(
586
+ category="config",
587
+ message="record export does not allow record_ids and where at the same time",
588
+ details={
589
+ "error_code": "EXPORT_ROW_SCOPE_CONFLICT",
590
+ "fix_hint": "Use record_ids for explicit selected rows or where for internal query selection, but not both.",
591
+ },
592
+ )
593
+ if selected_record_ids:
594
+ return selected_record_ids, "selected"
595
+ if not where_filters:
596
+ return [], "all"
597
+ resolved_ids = self._collect_record_ids_from_where(
598
+ profile=profile,
599
+ context=context,
600
+ app_key=app_key,
601
+ resolved_view=resolved_view,
602
+ where_filters=where_filters,
603
+ select_columns=select_columns,
604
+ )
605
+ if not resolved_ids:
606
+ raise QingflowApiError.config_error(
607
+ "record export query did not match any records",
608
+ details={
609
+ "error_code": "EXPORT_NO_MATCHED_RECORDS",
610
+ "view_id": resolved_view.view_id,
611
+ },
612
+ )
613
+ return resolved_ids, "queried"
614
+
615
+ def _collect_record_ids_from_where(
616
+ self,
617
+ *,
618
+ profile: str,
619
+ context,
620
+ app_key: str,
621
+ resolved_view: AccessibleViewRoute,
622
+ where_filters: list[JSONObject],
623
+ select_columns: list[JSONObject],
624
+ ) -> list[int]:
625
+ browse_scope = self._record_tools._build_browse_write_scope(
626
+ profile,
627
+ context,
628
+ app_key,
629
+ resolved_view,
630
+ force_refresh=False,
631
+ )
632
+ index = browse_scope["index"]
633
+ match_rules = self._record_tools._resolve_match_rules(context, where_filters, index)
634
+ dept_member_cache: dict[int, set[int]] = {}
635
+ current_page = 1
636
+ selected_ids: list[int] = []
637
+ seen: set[int] = set()
638
+ primary_field_ids = [
639
+ _coerce_int(item.get("queId"))
640
+ for item in select_columns
641
+ if isinstance(item, dict)
642
+ ]
643
+ primary_search_que_ids = [item for item in primary_field_ids if item is not None][:1] or None
644
+
645
+ while True:
646
+ page = self._record_tools._search_page(
647
+ context,
648
+ app_key=app_key,
649
+ view_selection=resolved_view.view_selection,
650
+ page_num=current_page,
651
+ page_size=DEFAULT_LIST_PAGE_SIZE,
652
+ query_key=None,
653
+ match_rules=match_rules,
654
+ sorts=[],
655
+ search_que_ids=primary_search_que_ids,
656
+ list_type=resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE,
657
+ )
658
+ raw_rows = page.get("list")
659
+ items = raw_rows if isinstance(raw_rows, list) else []
660
+ for item in items:
661
+ if not isinstance(item, dict):
662
+ continue
663
+ answers = item.get("answers")
664
+ answer_list = answers if isinstance(answers, list) else []
665
+ if not self._record_tools._matches_view_selection(
666
+ context,
667
+ answer_list,
668
+ view_selection=resolved_view.view_selection,
669
+ dept_member_cache=dept_member_cache,
670
+ ):
671
+ continue
672
+ record_id = _coerce_int(item.get("applyId"))
673
+ if record_id is None:
674
+ record_id = _coerce_int(item.get("apply_id"))
675
+ if record_id is None:
676
+ record_id = _coerce_int(item.get("id"))
677
+ if record_id is None:
678
+ record_id = _coerce_int(item.get("record_id"))
679
+ if record_id is None or record_id in seen or record_id <= 0:
680
+ continue
681
+ seen.add(record_id)
682
+ selected_ids.append(record_id)
683
+ if len(selected_ids) > EXPORT_ROWS_LIMIT:
684
+ raise QingflowApiError.config_error(
685
+ f"record export exceeds the native row limit of {EXPORT_ROWS_LIMIT}",
686
+ details={
687
+ "error_code": "EXPORT_ROWS_LIMIT_EXCEEDED",
688
+ "result_amount": len(selected_ids),
689
+ "row_limit": EXPORT_ROWS_LIMIT,
690
+ },
691
+ )
692
+ if current_page == 1:
693
+ reported_total = _effective_total(page, page_size=DEFAULT_LIST_PAGE_SIZE)
694
+ if reported_total > EXPORT_ROWS_LIMIT:
695
+ raise QingflowApiError.config_error(
696
+ f"record export exceeds the native row limit of {EXPORT_ROWS_LIMIT}",
697
+ details={
698
+ "error_code": "EXPORT_ROWS_LIMIT_EXCEEDED",
699
+ "result_amount": reported_total,
700
+ "row_limit": EXPORT_ROWS_LIMIT,
701
+ },
702
+ )
703
+ if not _page_has_more(page, current_page=current_page, page_size=DEFAULT_LIST_PAGE_SIZE, returned_rows=len(items)):
704
+ break
705
+ current_page += 1
706
+ return selected_ids
707
+
555
708
  def _build_export_filter_bean(
556
709
  self,
557
710
  resolved_view: AccessibleViewRoute,
@@ -1101,6 +1254,13 @@ def _effective_total(page: JSONObject, *, page_size: int) -> int:
1101
1254
  return returned_rows
1102
1255
 
1103
1256
 
1257
+ def _page_has_more(page: JSONObject, *, current_page: int, page_size: int, returned_rows: int) -> bool:
1258
+ page_amount = _coerce_int(page.get("pageAmount"))
1259
+ if page_amount is not None:
1260
+ return current_page < page_amount
1261
+ return returned_rows >= page_size
1262
+
1263
+
1104
1264
  def _resolve_download_targets(
1105
1265
  destination_hint: str,
1106
1266
  *,