@josephyan/qingflow-cli 0.2.0-beta.1010 → 0.2.0-beta.1011
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/cli/commands/exports.py +31 -0
- package/src/qingflow_mcp/cli/formatters.py +15 -0
- package/src/qingflow_mcp/server.py +7 -0
- package/src/qingflow_mcp/server_app_user.py +24 -1
- package/src/qingflow_mcp/tools/export_tools.py +224 -7
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.
|
|
6
|
+
npm install @josephyan/qingflow-cli@0.2.0-beta.1011
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-cli@0.2.0-beta.1011 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.
|
|
3
|
+
"version": "0.2.0-beta.1011",
|
|
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
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
import argparse
|
|
4
4
|
|
|
5
5
|
from ..context import CliContext
|
|
6
|
+
from .common import load_list_arg
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -12,6 +13,11 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
12
13
|
start = export_subparsers.add_parser("start", help="启动导出")
|
|
13
14
|
start.add_argument("--app-key", required=True)
|
|
14
15
|
start.add_argument("--view-id", default="system:all")
|
|
16
|
+
start.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
|
|
17
|
+
start.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
|
|
18
|
+
start.add_argument("--record-id", dest="record_ids", action="append", default=[], help="只导出这些 record_id;不传时导出当前视图全部数据")
|
|
19
|
+
start.add_argument("--record-ids-file", help="JSON/YAML list,内容与 --record-id 语义一致")
|
|
20
|
+
start.add_argument("--include-workflow-log", action=argparse.BooleanOptionalAction, default=False, help="是否同时导出流程日志")
|
|
15
21
|
start.set_defaults(handler=_handle_start, format_hint="export_start")
|
|
16
22
|
|
|
17
23
|
status = export_subparsers.add_parser("status", help="查询导出状态")
|
|
@@ -26,16 +32,38 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
26
32
|
direct = export_subparsers.add_parser("direct", help="直接导出并下载")
|
|
27
33
|
direct.add_argument("--app-key", required=True)
|
|
28
34
|
direct.add_argument("--view-id", default="system:all")
|
|
35
|
+
direct.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
|
|
36
|
+
direct.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
|
|
37
|
+
direct.add_argument("--record-id", dest="record_ids", action="append", default=[], help="只导出这些 record_id;不传时导出当前视图全部数据")
|
|
38
|
+
direct.add_argument("--record-ids-file", help="JSON/YAML list,内容与 --record-id 语义一致")
|
|
39
|
+
direct.add_argument("--include-workflow-log", action=argparse.BooleanOptionalAction, default=False, help="是否同时导出流程日志")
|
|
29
40
|
direct.add_argument("--download-to-path")
|
|
30
41
|
direct.add_argument("--wait-timeout-seconds", type=float)
|
|
31
42
|
direct.set_defaults(handler=_handle_direct, format_hint="export_direct")
|
|
32
43
|
|
|
33
44
|
|
|
45
|
+
def _columns(args: argparse.Namespace) -> list[int | dict]:
|
|
46
|
+
columns: list[int | dict] = list(args.columns or [])
|
|
47
|
+
if args.columns_file:
|
|
48
|
+
columns.extend(load_list_arg(args.columns_file, option_name="--columns-file"))
|
|
49
|
+
return columns
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _record_ids(args: argparse.Namespace) -> list[str | int]:
|
|
53
|
+
record_ids: list[str | int] = list(args.record_ids or [])
|
|
54
|
+
if args.record_ids_file:
|
|
55
|
+
record_ids.extend(load_list_arg(args.record_ids_file, option_name="--record-ids-file"))
|
|
56
|
+
return record_ids
|
|
57
|
+
|
|
58
|
+
|
|
34
59
|
def _handle_start(args: argparse.Namespace, context: CliContext) -> dict:
|
|
35
60
|
return context.exports.record_export_start(
|
|
36
61
|
profile=args.profile,
|
|
37
62
|
app_key=args.app_key,
|
|
38
63
|
view_id=args.view_id,
|
|
64
|
+
columns=_columns(args),
|
|
65
|
+
record_ids=_record_ids(args),
|
|
66
|
+
include_workflow_log=args.include_workflow_log,
|
|
39
67
|
)
|
|
40
68
|
|
|
41
69
|
|
|
@@ -59,6 +87,9 @@ def _handle_direct(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
59
87
|
profile=args.profile,
|
|
60
88
|
app_key=args.app_key,
|
|
61
89
|
view_id=args.view_id,
|
|
90
|
+
columns=_columns(args),
|
|
91
|
+
record_ids=_record_ids(args),
|
|
92
|
+
include_workflow_log=args.include_workflow_log,
|
|
62
93
|
download_to_path=args.download_to_path,
|
|
63
94
|
wait_timeout_seconds=args.wait_timeout_seconds,
|
|
64
95
|
)
|
|
@@ -426,6 +426,21 @@ def _format_export_common(result: dict[str, Any], *, title: str | None = None) -
|
|
|
426
426
|
f"Rows: {result.get('num') if result.get('num') is not None else '-'}",
|
|
427
427
|
]
|
|
428
428
|
)
|
|
429
|
+
row_scope = result.get("row_scope")
|
|
430
|
+
if row_scope not in (None, ""):
|
|
431
|
+
lines.append(f"Row Scope: {row_scope}")
|
|
432
|
+
selected_record_count = result.get("selected_record_count")
|
|
433
|
+
if selected_record_count not in (None, ""):
|
|
434
|
+
lines.append(f"Selected Rows: {selected_record_count}")
|
|
435
|
+
field_scope = result.get("field_scope")
|
|
436
|
+
if field_scope not in (None, ""):
|
|
437
|
+
lines.append(f"Field Scope: {field_scope}")
|
|
438
|
+
selected_field_count = result.get("selected_field_count")
|
|
439
|
+
if selected_field_count not in (None, ""):
|
|
440
|
+
lines.append(f"Selected Fields: {selected_field_count}")
|
|
441
|
+
include_workflow_log = result.get("include_workflow_log")
|
|
442
|
+
if include_workflow_log not in (None, ""):
|
|
443
|
+
lines.append(f"Include Workflow Log: {include_workflow_log}")
|
|
429
444
|
file_names = result.get("file_names") if isinstance(result.get("file_names"), list) else []
|
|
430
445
|
file_urls = result.get("file_urls") if isinstance(result.get("file_urls"), list) else []
|
|
431
446
|
if file_names or file_urls:
|
|
@@ -155,6 +155,13 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
155
155
|
|
|
156
156
|
- `record_export_direct` is the one-shot export path that starts the export, waits for completion, downloads locally, and still returns remote download links.
|
|
157
157
|
- Export v1 supports record views only and follows the same public `view_id` semantics as `record_list` (`system:*` and `custom:*`).
|
|
158
|
+
- `record_export_start` / `record_export_direct` support frontend-like row selection:
|
|
159
|
+
- omit `record_ids` to export all rows in the selected view
|
|
160
|
+
- pass `record_ids` to export selected rows only
|
|
161
|
+
- `record_export_start` / `record_export_direct` also support frontend-like column selection:
|
|
162
|
+
- omit `columns` to export all current-view fields
|
|
163
|
+
- pass `columns` to export only selected fields, preserving the provided order
|
|
164
|
+
- `include_workflow_log=true` maps to the native workflow-log export switch.
|
|
158
165
|
|
|
159
166
|
## Task Workflow Path
|
|
160
167
|
|
|
@@ -150,6 +150,13 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
150
150
|
|
|
151
151
|
- `record_export_direct` is the one-shot path that starts export, waits, downloads locally, and still returns remote download links.
|
|
152
152
|
- Export v1 supports record views only and follows the same public `view_id` semantics as `record_list`.
|
|
153
|
+
- `record_export_start` / `record_export_direct` support frontend-like row selection:
|
|
154
|
+
- omit `record_ids` to export all rows in the selected view
|
|
155
|
+
- pass `record_ids` to export selected rows only
|
|
156
|
+
- `record_export_start` / `record_export_direct` also support frontend-like column selection:
|
|
157
|
+
- omit `columns` to export all current-view fields
|
|
158
|
+
- pass `columns` to export only selected fields, preserving the provided order
|
|
159
|
+
- `include_workflow_log=true` maps to the native workflow-log export switch.
|
|
153
160
|
|
|
154
161
|
## Task Workflow Path
|
|
155
162
|
|
|
@@ -233,8 +240,18 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
233
240
|
profile: str = DEFAULT_PROFILE,
|
|
234
241
|
app_key: str = "",
|
|
235
242
|
view_id: str = "system:all",
|
|
243
|
+
columns: list[dict | int] | None = None,
|
|
244
|
+
record_ids: list[str | int] | None = None,
|
|
245
|
+
include_workflow_log: bool = False,
|
|
236
246
|
) -> dict:
|
|
237
|
-
return exports.record_export_start(
|
|
247
|
+
return exports.record_export_start(
|
|
248
|
+
profile=profile,
|
|
249
|
+
app_key=app_key,
|
|
250
|
+
view_id=view_id,
|
|
251
|
+
columns=columns or [],
|
|
252
|
+
record_ids=record_ids or [],
|
|
253
|
+
include_workflow_log=include_workflow_log,
|
|
254
|
+
)
|
|
238
255
|
|
|
239
256
|
@server.tool()
|
|
240
257
|
def record_export_status_get(
|
|
@@ -260,6 +277,9 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
260
277
|
profile: str = DEFAULT_PROFILE,
|
|
261
278
|
app_key: str = "",
|
|
262
279
|
view_id: str = "system:all",
|
|
280
|
+
columns: list[dict | int] | None = None,
|
|
281
|
+
record_ids: list[str | int] | None = None,
|
|
282
|
+
include_workflow_log: bool = False,
|
|
263
283
|
download_to_path: str | None = None,
|
|
264
284
|
wait_timeout_seconds: float | None = None,
|
|
265
285
|
) -> dict:
|
|
@@ -267,6 +287,9 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
267
287
|
profile=profile,
|
|
268
288
|
app_key=app_key,
|
|
269
289
|
view_id=view_id,
|
|
290
|
+
columns=columns or [],
|
|
291
|
+
record_ids=record_ids or [],
|
|
292
|
+
include_workflow_log=include_workflow_log,
|
|
270
293
|
download_to_path=download_to_path,
|
|
271
294
|
wait_timeout_seconds=wait_timeout_seconds,
|
|
272
295
|
)
|
|
@@ -11,12 +11,17 @@ from uuid import uuid4
|
|
|
11
11
|
|
|
12
12
|
from mcp.server.fastmcp import FastMCP
|
|
13
13
|
|
|
14
|
-
from ..config import DEFAULT_PROFILE
|
|
14
|
+
from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE
|
|
15
15
|
from ..errors import QingflowApiError
|
|
16
16
|
from ..export_store import ExportJobStore
|
|
17
17
|
from ..json_types import JSONObject
|
|
18
18
|
from .base import ToolBase, tool_cn_name
|
|
19
|
-
from .record_tools import
|
|
19
|
+
from .record_tools import (
|
|
20
|
+
AccessibleViewRoute,
|
|
21
|
+
LAYOUT_ONLY_QUE_TYPES,
|
|
22
|
+
RecordTools,
|
|
23
|
+
_normalize_public_column_selectors,
|
|
24
|
+
)
|
|
20
25
|
|
|
21
26
|
|
|
22
27
|
EXPORT_STATUS_BY_PROCESS_STATUS = {
|
|
@@ -28,6 +33,8 @@ EXPORT_STATUS_BY_PROCESS_STATUS = {
|
|
|
28
33
|
}
|
|
29
34
|
DEFAULT_EXPORT_TIMEOUT_SECONDS = 60.0
|
|
30
35
|
EXPORT_POLL_INTERVAL_SECONDS = 1.5
|
|
36
|
+
EXPORT_ROWS_LIMIT = 20_000
|
|
37
|
+
EXPORT_CELL_LIMIT = 4_000_000
|
|
31
38
|
_SAFE_FILE_CHARS = re.compile(r"[^0-9A-Za-z._-]+")
|
|
32
39
|
LOCAL_TIMEZONE = datetime.now().astimezone().tzinfo or timezone.utc
|
|
33
40
|
|
|
@@ -50,11 +57,17 @@ class ExportTools(ToolBase):
|
|
|
50
57
|
profile: str = DEFAULT_PROFILE,
|
|
51
58
|
app_key: str = "",
|
|
52
59
|
view_id: str = "system:all",
|
|
60
|
+
columns: list[JSONObject | int] | None = None,
|
|
61
|
+
record_ids: list[str | int] | None = None,
|
|
62
|
+
include_workflow_log: bool = False,
|
|
53
63
|
) -> dict[str, Any]:
|
|
54
64
|
return self.record_export_start(
|
|
55
65
|
profile=profile,
|
|
56
66
|
app_key=app_key,
|
|
57
67
|
view_id=view_id,
|
|
68
|
+
columns=columns or [],
|
|
69
|
+
record_ids=record_ids or [],
|
|
70
|
+
include_workflow_log=include_workflow_log,
|
|
58
71
|
)
|
|
59
72
|
|
|
60
73
|
@mcp.tool(description="Get asynchronous record export status by opaque export_handle.")
|
|
@@ -84,6 +97,9 @@ class ExportTools(ToolBase):
|
|
|
84
97
|
profile: str = DEFAULT_PROFILE,
|
|
85
98
|
app_key: str = "",
|
|
86
99
|
view_id: str = "system:all",
|
|
100
|
+
columns: list[JSONObject | int] | None = None,
|
|
101
|
+
record_ids: list[str | int] | None = None,
|
|
102
|
+
include_workflow_log: bool = False,
|
|
87
103
|
download_to_path: str | None = None,
|
|
88
104
|
wait_timeout_seconds: float | None = None,
|
|
89
105
|
) -> dict[str, Any]:
|
|
@@ -91,6 +107,9 @@ class ExportTools(ToolBase):
|
|
|
91
107
|
profile=profile,
|
|
92
108
|
app_key=app_key,
|
|
93
109
|
view_id=view_id,
|
|
110
|
+
columns=columns or [],
|
|
111
|
+
record_ids=record_ids or [],
|
|
112
|
+
include_workflow_log=include_workflow_log,
|
|
94
113
|
download_to_path=download_to_path,
|
|
95
114
|
wait_timeout_seconds=wait_timeout_seconds,
|
|
96
115
|
)
|
|
@@ -102,9 +121,14 @@ class ExportTools(ToolBase):
|
|
|
102
121
|
profile: str = DEFAULT_PROFILE,
|
|
103
122
|
app_key: str,
|
|
104
123
|
view_id: str = "system:all",
|
|
124
|
+
columns: list[JSONObject | int] | None = None,
|
|
125
|
+
record_ids: list[str | int] | None = None,
|
|
126
|
+
include_workflow_log: bool = False,
|
|
105
127
|
) -> dict[str, Any]:
|
|
106
128
|
normalized_app_key = str(app_key or "").strip()
|
|
107
129
|
normalized_view_id = str(view_id or "").strip() or "system:all"
|
|
130
|
+
normalized_columns = _normalize_export_columns(columns or [])
|
|
131
|
+
normalized_record_ids = _normalize_export_record_ids(record_ids or [])
|
|
108
132
|
if not normalized_app_key:
|
|
109
133
|
return self._failed_export_result(
|
|
110
134
|
error_code="EXPORT_START_FAILED",
|
|
@@ -128,8 +152,24 @@ class ExportTools(ToolBase):
|
|
|
128
152
|
context=context,
|
|
129
153
|
app_key=normalized_app_key,
|
|
130
154
|
resolved_view=resolved_view,
|
|
155
|
+
column_selectors=normalized_columns,
|
|
156
|
+
)
|
|
157
|
+
result_amount = self._estimate_export_result_amount(
|
|
158
|
+
context,
|
|
159
|
+
app_key=normalized_app_key,
|
|
160
|
+
resolved_view=resolved_view,
|
|
161
|
+
selected_record_ids=normalized_record_ids,
|
|
162
|
+
)
|
|
163
|
+
self._validate_export_limits(
|
|
164
|
+
result_amount=result_amount,
|
|
165
|
+
selected_field_count=len(cast(list[JSONObject], export_config.get("questionExportConfigList") or [])),
|
|
166
|
+
include_workflow_log=include_workflow_log,
|
|
167
|
+
)
|
|
168
|
+
filter_bean = self._build_export_filter_bean(
|
|
169
|
+
resolved_view,
|
|
170
|
+
selected_record_ids=normalized_record_ids,
|
|
171
|
+
include_workflow_log=include_workflow_log,
|
|
131
172
|
)
|
|
132
|
-
filter_bean = self._build_export_filter_bean(resolved_view)
|
|
133
173
|
started_at = _utc_now().replace(microsecond=0).isoformat()
|
|
134
174
|
socket_result = self.backend.start_socket_record_export(
|
|
135
175
|
context,
|
|
@@ -138,7 +178,10 @@ class ExportTools(ToolBase):
|
|
|
138
178
|
view_key=resolved_view.view_selection.view_key if resolved_view.view_selection is not None else None,
|
|
139
179
|
filter_bean=filter_bean,
|
|
140
180
|
export_config=export_config,
|
|
181
|
+
result_amount=result_amount,
|
|
141
182
|
)
|
|
183
|
+
row_scope = "selected" if normalized_record_ids else "all"
|
|
184
|
+
column_scope = "selected" if normalized_columns else "all"
|
|
142
185
|
export_handle = uuid4().hex
|
|
143
186
|
self._job_store.put(
|
|
144
187
|
export_handle,
|
|
@@ -150,6 +193,11 @@ class ExportTools(ToolBase):
|
|
|
150
193
|
"backend_export_id": str(socket_result.get("backend_export_id") or ""),
|
|
151
194
|
"started_at": started_at,
|
|
152
195
|
"uid": session_profile.uid,
|
|
196
|
+
"row_scope": row_scope,
|
|
197
|
+
"selected_record_count": len(normalized_record_ids),
|
|
198
|
+
"field_scope": column_scope,
|
|
199
|
+
"selected_field_count": len(cast(list[JSONObject], export_config.get("questionExportConfigList") or [])),
|
|
200
|
+
"include_workflow_log": bool(include_workflow_log),
|
|
153
201
|
},
|
|
154
202
|
)
|
|
155
203
|
warnings = [
|
|
@@ -163,6 +211,11 @@ class ExportTools(ToolBase):
|
|
|
163
211
|
"app_key": normalized_app_key,
|
|
164
212
|
"view_id": resolved_view.view_id,
|
|
165
213
|
"export_handle": export_handle,
|
|
214
|
+
"row_scope": row_scope,
|
|
215
|
+
"selected_record_count": len(normalized_record_ids),
|
|
216
|
+
"field_scope": column_scope,
|
|
217
|
+
"selected_field_count": len(cast(list[JSONObject], export_config.get("questionExportConfigList") or [])),
|
|
218
|
+
"include_workflow_log": bool(include_workflow_log),
|
|
166
219
|
"file_urls": [],
|
|
167
220
|
"file_names": [],
|
|
168
221
|
"downloaded_files": [],
|
|
@@ -170,6 +223,8 @@ class ExportTools(ToolBase):
|
|
|
170
223
|
"verification": {
|
|
171
224
|
"export_acknowledged": bool(socket_result.get("backend_export_id")),
|
|
172
225
|
"view_route_supported": True,
|
|
226
|
+
"row_selection_applied": bool(normalized_record_ids),
|
|
227
|
+
"field_selection_applied": bool(normalized_columns),
|
|
173
228
|
},
|
|
174
229
|
"request_route": self.backend.describe_route(context),
|
|
175
230
|
}
|
|
@@ -253,6 +308,11 @@ class ExportTools(ToolBase):
|
|
|
253
308
|
"export_handle": normalized_handle,
|
|
254
309
|
"app_key": str(local_job.get("app_key") or ""),
|
|
255
310
|
"view_id": str(local_job.get("view_id") or ""),
|
|
311
|
+
"row_scope": str(local_job.get("row_scope") or "all"),
|
|
312
|
+
"selected_record_count": _coerce_int(local_job.get("selected_record_count")) or 0,
|
|
313
|
+
"field_scope": str(local_job.get("field_scope") or "all"),
|
|
314
|
+
"selected_field_count": _coerce_int(local_job.get("selected_field_count")),
|
|
315
|
+
"include_workflow_log": bool(local_job.get("include_workflow_log")),
|
|
256
316
|
"file_urls": snapshot.get("file_urls") or [],
|
|
257
317
|
"file_names": snapshot.get("file_names") or [],
|
|
258
318
|
"downloaded_files": [],
|
|
@@ -270,6 +330,11 @@ class ExportTools(ToolBase):
|
|
|
270
330
|
"export_handle": normalized_handle,
|
|
271
331
|
"app_key": str(local_job.get("app_key") or ""),
|
|
272
332
|
"view_id": str(local_job.get("view_id") or ""),
|
|
333
|
+
"row_scope": str(local_job.get("row_scope") or "all"),
|
|
334
|
+
"selected_record_count": _coerce_int(local_job.get("selected_record_count")) or 0,
|
|
335
|
+
"field_scope": str(local_job.get("field_scope") or "all"),
|
|
336
|
+
"selected_field_count": _coerce_int(local_job.get("selected_field_count")),
|
|
337
|
+
"include_workflow_log": bool(local_job.get("include_workflow_log")),
|
|
273
338
|
"file_urls": snapshot.get("file_urls") or [],
|
|
274
339
|
"file_names": snapshot.get("file_names") or [],
|
|
275
340
|
"downloaded_files": [],
|
|
@@ -308,6 +373,11 @@ class ExportTools(ToolBase):
|
|
|
308
373
|
"export_handle": normalized_handle,
|
|
309
374
|
"app_key": str(local_job.get("app_key") or ""),
|
|
310
375
|
"view_id": str(local_job.get("view_id") or ""),
|
|
376
|
+
"row_scope": str(local_job.get("row_scope") or "all"),
|
|
377
|
+
"selected_record_count": _coerce_int(local_job.get("selected_record_count")) or 0,
|
|
378
|
+
"field_scope": str(local_job.get("field_scope") or "all"),
|
|
379
|
+
"selected_field_count": _coerce_int(local_job.get("selected_field_count")),
|
|
380
|
+
"include_workflow_log": bool(local_job.get("include_workflow_log")),
|
|
311
381
|
"num": snapshot.get("num"),
|
|
312
382
|
"process_status": snapshot.get("process_status"),
|
|
313
383
|
"error_code": snapshot.get("error_code"),
|
|
@@ -336,6 +406,9 @@ class ExportTools(ToolBase):
|
|
|
336
406
|
profile: str = DEFAULT_PROFILE,
|
|
337
407
|
app_key: str,
|
|
338
408
|
view_id: str = "system:all",
|
|
409
|
+
columns: list[JSONObject | int] | None = None,
|
|
410
|
+
record_ids: list[str | int] | None = None,
|
|
411
|
+
include_workflow_log: bool = False,
|
|
339
412
|
download_to_path: str | None = None,
|
|
340
413
|
wait_timeout_seconds: float | None = None,
|
|
341
414
|
) -> dict[str, Any]:
|
|
@@ -354,6 +427,9 @@ class ExportTools(ToolBase):
|
|
|
354
427
|
profile=profile,
|
|
355
428
|
app_key=normalized_app_key,
|
|
356
429
|
view_id=normalized_view_id,
|
|
430
|
+
columns=columns or [],
|
|
431
|
+
record_ids=record_ids or [],
|
|
432
|
+
include_workflow_log=include_workflow_log,
|
|
357
433
|
)
|
|
358
434
|
if not bool(start_result.get("ok")):
|
|
359
435
|
return start_result
|
|
@@ -395,6 +471,11 @@ class ExportTools(ToolBase):
|
|
|
395
471
|
"export_handle": export_handle,
|
|
396
472
|
"app_key": str(local_job.get("app_key") or ""),
|
|
397
473
|
"view_id": str(local_job.get("view_id") or ""),
|
|
474
|
+
"row_scope": str(local_job.get("row_scope") or "all"),
|
|
475
|
+
"selected_record_count": _coerce_int(local_job.get("selected_record_count")) or 0,
|
|
476
|
+
"field_scope": str(local_job.get("field_scope") or "all"),
|
|
477
|
+
"selected_field_count": _coerce_int(local_job.get("selected_field_count")),
|
|
478
|
+
"include_workflow_log": bool(local_job.get("include_workflow_log")),
|
|
398
479
|
"num": snapshot.get("num"),
|
|
399
480
|
"process_status": snapshot.get("process_status"),
|
|
400
481
|
"audit_record_status": snapshot.get("audit_record_status"),
|
|
@@ -417,6 +498,11 @@ class ExportTools(ToolBase):
|
|
|
417
498
|
"export_handle": export_handle,
|
|
418
499
|
"app_key": str(local_job.get("app_key") or ""),
|
|
419
500
|
"view_id": str(local_job.get("view_id") or ""),
|
|
501
|
+
"row_scope": str(local_job.get("row_scope") or "all"),
|
|
502
|
+
"selected_record_count": _coerce_int(local_job.get("selected_record_count")) or 0,
|
|
503
|
+
"field_scope": str(local_job.get("field_scope") or "all"),
|
|
504
|
+
"selected_field_count": _coerce_int(local_job.get("selected_field_count")),
|
|
505
|
+
"include_workflow_log": bool(local_job.get("include_workflow_log")),
|
|
420
506
|
"file_urls": snapshot.get("file_urls") or [],
|
|
421
507
|
"file_names": snapshot.get("file_names") or [],
|
|
422
508
|
"downloaded_files": [],
|
|
@@ -444,6 +530,11 @@ class ExportTools(ToolBase):
|
|
|
444
530
|
"export_handle": str(start_result.get("export_handle") or ""),
|
|
445
531
|
"app_key": normalized_app_key,
|
|
446
532
|
"view_id": str(start_result.get("view_id") or normalized_view_id),
|
|
533
|
+
"row_scope": start_result.get("row_scope") or ("selected" if record_ids else "all"),
|
|
534
|
+
"selected_record_count": start_result.get("selected_record_count") or 0,
|
|
535
|
+
"field_scope": start_result.get("field_scope") or ("selected" if columns else "all"),
|
|
536
|
+
"selected_field_count": start_result.get("selected_field_count"),
|
|
537
|
+
"include_workflow_log": start_result.get("include_workflow_log"),
|
|
447
538
|
"file_urls": (last_snapshot or {}).get("file_urls") or [],
|
|
448
539
|
"file_names": (last_snapshot or {}).get("file_names") or [],
|
|
449
540
|
"downloaded_files": [],
|
|
@@ -461,15 +552,23 @@ class ExportTools(ToolBase):
|
|
|
461
552
|
extra={"app_key": normalized_app_key, "view_id": normalized_view_id},
|
|
462
553
|
)
|
|
463
554
|
|
|
464
|
-
def _build_export_filter_bean(
|
|
555
|
+
def _build_export_filter_bean(
|
|
556
|
+
self,
|
|
557
|
+
resolved_view: AccessibleViewRoute,
|
|
558
|
+
*,
|
|
559
|
+
selected_record_ids: list[int],
|
|
560
|
+
include_workflow_log: bool,
|
|
561
|
+
) -> JSONObject:
|
|
465
562
|
filter_payload: JSONObject = {}
|
|
466
563
|
if resolved_view.kind == "system" and resolved_view.list_type is not None:
|
|
467
564
|
filter_payload["type"] = resolved_view.list_type
|
|
565
|
+
if selected_record_ids:
|
|
566
|
+
filter_payload["applyIds"] = selected_record_ids
|
|
468
567
|
return {
|
|
469
568
|
"filter": filter_payload,
|
|
470
569
|
# Backend export code later auto-unboxes this field to primitive boolean.
|
|
471
|
-
#
|
|
472
|
-
"auditRecordStatus":
|
|
570
|
+
# Always send an explicit boolean to avoid a null -> NPE -> 41100 failure path.
|
|
571
|
+
"auditRecordStatus": bool(include_workflow_log),
|
|
473
572
|
}
|
|
474
573
|
|
|
475
574
|
def _build_export_config(
|
|
@@ -479,6 +578,7 @@ class ExportTools(ToolBase):
|
|
|
479
578
|
context,
|
|
480
579
|
app_key: str,
|
|
481
580
|
resolved_view: AccessibleViewRoute,
|
|
581
|
+
column_selectors: list[int],
|
|
482
582
|
) -> tuple[JSONObject, list[JSONObject]]: # type: ignore[no-untyped-def]
|
|
483
583
|
browse_scope = self._record_tools._build_browse_write_scope(
|
|
484
584
|
profile,
|
|
@@ -507,9 +607,32 @@ class ExportTools(ToolBase):
|
|
|
507
607
|
for field in cast(Any, index).by_id.values()
|
|
508
608
|
if field.que_type not in LAYOUT_ONLY_QUE_TYPES
|
|
509
609
|
]
|
|
610
|
+
exportable_by_id = {field.que_id: field for field in ordered_visible_fields}
|
|
611
|
+
selected_fields: list[Any]
|
|
612
|
+
if column_selectors:
|
|
613
|
+
selected_fields = []
|
|
614
|
+
seen: set[int] = set()
|
|
615
|
+
for selector in column_selectors:
|
|
616
|
+
field = self._record_tools._resolve_field_selector(selector, index, location="export_columns")
|
|
617
|
+
if field.que_id in seen:
|
|
618
|
+
continue
|
|
619
|
+
exportable_field = exportable_by_id.get(field.que_id)
|
|
620
|
+
if exportable_field is None:
|
|
621
|
+
raise QingflowApiError.config_error(
|
|
622
|
+
f"field '{field.que_title}' is not exportable in the selected view",
|
|
623
|
+
details={
|
|
624
|
+
"error_code": "EXPORT_FIELD_NOT_VISIBLE",
|
|
625
|
+
"field_id": field.que_id,
|
|
626
|
+
"view_id": resolved_view.view_id,
|
|
627
|
+
},
|
|
628
|
+
)
|
|
629
|
+
selected_fields.append(exportable_field)
|
|
630
|
+
seen.add(field.que_id)
|
|
631
|
+
else:
|
|
632
|
+
selected_fields = ordered_visible_fields
|
|
510
633
|
question_export_config_list: list[JSONObject] = []
|
|
511
634
|
warnings: list[JSONObject] = []
|
|
512
|
-
for field in
|
|
635
|
+
for field in selected_fields:
|
|
513
636
|
if field.que_type is None:
|
|
514
637
|
warnings.append(
|
|
515
638
|
{
|
|
@@ -533,6 +656,61 @@ class ExportTools(ToolBase):
|
|
|
533
656
|
)
|
|
534
657
|
return {"questionExportConfigList": question_export_config_list}, warnings
|
|
535
658
|
|
|
659
|
+
def _estimate_export_result_amount(
|
|
660
|
+
self,
|
|
661
|
+
context,
|
|
662
|
+
*,
|
|
663
|
+
app_key: str,
|
|
664
|
+
resolved_view: AccessibleViewRoute,
|
|
665
|
+
selected_record_ids: list[int],
|
|
666
|
+
) -> int:
|
|
667
|
+
if selected_record_ids:
|
|
668
|
+
return len(selected_record_ids)
|
|
669
|
+
page = self._record_tools._search_page(
|
|
670
|
+
context,
|
|
671
|
+
app_key=app_key,
|
|
672
|
+
view_selection=resolved_view.view_selection,
|
|
673
|
+
page_num=1,
|
|
674
|
+
page_size=1,
|
|
675
|
+
query_key=None,
|
|
676
|
+
match_rules=[],
|
|
677
|
+
sorts=[],
|
|
678
|
+
search_que_ids=None,
|
|
679
|
+
list_type=resolved_view.list_type if resolved_view.list_type is not None else DEFAULT_RECORD_LIST_TYPE,
|
|
680
|
+
)
|
|
681
|
+
result_amount = _effective_total(page, page_size=1)
|
|
682
|
+
if result_amount < 0:
|
|
683
|
+
result_amount = 0
|
|
684
|
+
return result_amount
|
|
685
|
+
|
|
686
|
+
def _validate_export_limits(
|
|
687
|
+
self,
|
|
688
|
+
*,
|
|
689
|
+
result_amount: int,
|
|
690
|
+
selected_field_count: int,
|
|
691
|
+
include_workflow_log: bool,
|
|
692
|
+
) -> None:
|
|
693
|
+
if result_amount > EXPORT_ROWS_LIMIT:
|
|
694
|
+
raise QingflowApiError.config_error(
|
|
695
|
+
f"record export exceeds the native row limit of {EXPORT_ROWS_LIMIT}",
|
|
696
|
+
details={
|
|
697
|
+
"error_code": "EXPORT_ROWS_LIMIT_EXCEEDED",
|
|
698
|
+
"result_amount": result_amount,
|
|
699
|
+
"row_limit": EXPORT_ROWS_LIMIT,
|
|
700
|
+
},
|
|
701
|
+
)
|
|
702
|
+
estimated_cells = max(result_amount, 0) * max(selected_field_count, 0)
|
|
703
|
+
if estimated_cells > EXPORT_CELL_LIMIT:
|
|
704
|
+
raise QingflowApiError.config_error(
|
|
705
|
+
f"record export exceeds the native cell limit of {EXPORT_CELL_LIMIT}",
|
|
706
|
+
details={
|
|
707
|
+
"error_code": "EXPORT_CELLS_LIMIT_EXCEEDED",
|
|
708
|
+
"estimated_cells": estimated_cells,
|
|
709
|
+
"cell_limit": EXPORT_CELL_LIMIT,
|
|
710
|
+
"include_workflow_log": include_workflow_log,
|
|
711
|
+
},
|
|
712
|
+
)
|
|
713
|
+
|
|
536
714
|
def _resolve_export_snapshot(
|
|
537
715
|
self,
|
|
538
716
|
context,
|
|
@@ -651,6 +829,11 @@ class ExportTools(ToolBase):
|
|
|
651
829
|
"export_handle": export_handle,
|
|
652
830
|
"app_key": str(local_job.get("app_key") or ""),
|
|
653
831
|
"view_id": str(local_job.get("view_id") or ""),
|
|
832
|
+
"row_scope": str(local_job.get("row_scope") or "all"),
|
|
833
|
+
"selected_record_count": _coerce_int(local_job.get("selected_record_count")) or 0,
|
|
834
|
+
"field_scope": str(local_job.get("field_scope") or "all"),
|
|
835
|
+
"selected_field_count": _coerce_int(local_job.get("selected_field_count")),
|
|
836
|
+
"include_workflow_log": bool(local_job.get("include_workflow_log")),
|
|
654
837
|
"num": snapshot.get("num"),
|
|
655
838
|
"process_status": snapshot.get("process_status"),
|
|
656
839
|
"error_code": snapshot.get("error_code"),
|
|
@@ -884,6 +1067,40 @@ def _coerce_int(value: Any) -> int | None:
|
|
|
884
1067
|
return None
|
|
885
1068
|
|
|
886
1069
|
|
|
1070
|
+
def _normalize_export_columns(columns: list[JSONObject | int]) -> list[int]:
|
|
1071
|
+
normalized: list[int] = []
|
|
1072
|
+
for field_id in _normalize_public_column_selectors(columns):
|
|
1073
|
+
if field_id not in normalized:
|
|
1074
|
+
normalized.append(field_id)
|
|
1075
|
+
return normalized
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _normalize_export_record_ids(record_ids: list[str | int]) -> list[int]:
|
|
1079
|
+
normalized: list[int] = []
|
|
1080
|
+
seen: set[int] = set()
|
|
1081
|
+
for item in record_ids:
|
|
1082
|
+
record_id = _coerce_int(item)
|
|
1083
|
+
if record_id is None or record_id <= 0 or record_id in seen:
|
|
1084
|
+
continue
|
|
1085
|
+
normalized.append(record_id)
|
|
1086
|
+
seen.add(record_id)
|
|
1087
|
+
return normalized
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
def _effective_total(page: JSONObject, *, page_size: int) -> int:
|
|
1091
|
+
rows = page.get("list")
|
|
1092
|
+
returned_rows = len(rows) if isinstance(rows, list) else 0
|
|
1093
|
+
reported = _coerce_int(page.get("total"))
|
|
1094
|
+
if reported is None:
|
|
1095
|
+
reported = _coerce_int(page.get("count"))
|
|
1096
|
+
if reported is not None:
|
|
1097
|
+
return max(reported, returned_rows)
|
|
1098
|
+
page_amount = _coerce_int(page.get("pageAmount"))
|
|
1099
|
+
if page_amount is not None:
|
|
1100
|
+
return page_amount * page_size
|
|
1101
|
+
return returned_rows
|
|
1102
|
+
|
|
1103
|
+
|
|
887
1104
|
def _resolve_download_targets(
|
|
888
1105
|
destination_hint: str,
|
|
889
1106
|
*,
|