@josephyan/qingflow-app-builder-mcp 0.2.0-beta.1008 → 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/backend_client.py +109 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/exports.py +95 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +71 -0
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/public_surface.py +4 -0
- package/src/qingflow_mcp/response_trim.py +14 -0
- package/src/qingflow_mcp/server.py +17 -0
- package/src/qingflow_mcp/server_app_user.py +76 -0
- package/src/qingflow_mcp/tools/export_tools.py +1177 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +30 -1
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.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1011
|
|
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.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1011 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -474,6 +474,115 @@ class BackendClient:
|
|
|
474
474
|
pass
|
|
475
475
|
return import_result
|
|
476
476
|
|
|
477
|
+
def start_socket_record_export(
|
|
478
|
+
self,
|
|
479
|
+
context: BackendRequestContext,
|
|
480
|
+
*,
|
|
481
|
+
app_key: str,
|
|
482
|
+
view_id: str,
|
|
483
|
+
filter_bean: JSONObject,
|
|
484
|
+
export_config: JSONObject,
|
|
485
|
+
view_key: str | None = None,
|
|
486
|
+
result_amount: int = 0,
|
|
487
|
+
ack_timeout_seconds: float = 8.0,
|
|
488
|
+
) -> dict[str, Any]:
|
|
489
|
+
try:
|
|
490
|
+
import socketio # type: ignore[import-not-found]
|
|
491
|
+
except ImportError as exc:
|
|
492
|
+
raise QingflowApiError(
|
|
493
|
+
category="config",
|
|
494
|
+
message=f"socket.io client dependency is missing: {exc}",
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
socket_base_url = self._build_socket_base_url(context.base_url)
|
|
498
|
+
export_result: dict[str, Any] = {
|
|
499
|
+
"backend_export_id": None,
|
|
500
|
+
"warnings": [],
|
|
501
|
+
}
|
|
502
|
+
sio = socketio.Client(reconnection=False, logger=False, engineio_logger=False)
|
|
503
|
+
event_name = "excelViewgraph" if view_key else "excel"
|
|
504
|
+
event_args: tuple[Any, ...]
|
|
505
|
+
if view_key:
|
|
506
|
+
event_args = (
|
|
507
|
+
context.token,
|
|
508
|
+
None,
|
|
509
|
+
view_key,
|
|
510
|
+
filter_bean,
|
|
511
|
+
export_config,
|
|
512
|
+
int(result_amount),
|
|
513
|
+
)
|
|
514
|
+
else:
|
|
515
|
+
event_args = (
|
|
516
|
+
context.token,
|
|
517
|
+
app_key,
|
|
518
|
+
filter_bean,
|
|
519
|
+
export_config,
|
|
520
|
+
int(result_amount),
|
|
521
|
+
)
|
|
522
|
+
try:
|
|
523
|
+
sio.connect(
|
|
524
|
+
socket_base_url,
|
|
525
|
+
transports=["websocket"],
|
|
526
|
+
socketio_path="socket.io",
|
|
527
|
+
headers=self._base_headers(
|
|
528
|
+
context.token,
|
|
529
|
+
context.ws_id,
|
|
530
|
+
qf_version=context.qf_version,
|
|
531
|
+
),
|
|
532
|
+
wait_timeout=ack_timeout_seconds,
|
|
533
|
+
)
|
|
534
|
+
sio.emit("token", context.token)
|
|
535
|
+
sleep(0.2)
|
|
536
|
+
ack = sio.call(
|
|
537
|
+
event_name,
|
|
538
|
+
event_args,
|
|
539
|
+
timeout=ack_timeout_seconds,
|
|
540
|
+
)
|
|
541
|
+
ack_payload = ack[0] if isinstance(ack, list) and ack else ack
|
|
542
|
+
export_id: Any = ack_payload
|
|
543
|
+
if isinstance(ack_payload, dict):
|
|
544
|
+
error_code = ack_payload.get("error")
|
|
545
|
+
ack_message = ack_payload.get("message")
|
|
546
|
+
export_id = ack_payload.get("data")
|
|
547
|
+
if isinstance(export_id, dict):
|
|
548
|
+
export_id = (
|
|
549
|
+
export_id.get("exportId")
|
|
550
|
+
or export_id.get("export_id")
|
|
551
|
+
or export_id.get("id")
|
|
552
|
+
)
|
|
553
|
+
if error_code not in (None, 0):
|
|
554
|
+
raise QingflowApiError(
|
|
555
|
+
category="backend",
|
|
556
|
+
message=str(ack_message or f"socket export rejected with error {error_code}"),
|
|
557
|
+
details={
|
|
558
|
+
"socket_error_code": error_code,
|
|
559
|
+
"app_key": app_key,
|
|
560
|
+
"view_id": view_id,
|
|
561
|
+
"view_key": view_key,
|
|
562
|
+
},
|
|
563
|
+
)
|
|
564
|
+
if not export_id:
|
|
565
|
+
raise QingflowApiError(category="backend", message="socket export ack did not return export_id")
|
|
566
|
+
export_result["backend_export_id"] = str(export_id)
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
message = str(exc)
|
|
569
|
+
if "timeout" in message.lower():
|
|
570
|
+
raise QingflowApiError(
|
|
571
|
+
category="network",
|
|
572
|
+
message="socket export ack timed out",
|
|
573
|
+
details={"error_code": "EXPORT_SOCKET_ACK_TIMEOUT"},
|
|
574
|
+
)
|
|
575
|
+
if isinstance(exc, QingflowApiError):
|
|
576
|
+
raise
|
|
577
|
+
raise QingflowApiError(category="network", message=message or "socket export failed")
|
|
578
|
+
finally:
|
|
579
|
+
try:
|
|
580
|
+
if sio.connected:
|
|
581
|
+
sio.disconnect()
|
|
582
|
+
except Exception:
|
|
583
|
+
pass
|
|
584
|
+
return export_result
|
|
585
|
+
|
|
477
586
|
def _request_with_meta(
|
|
478
587
|
self,
|
|
479
588
|
method: str,
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
|
|
5
|
-
from . import app, auth, builder, chart, imports, portal, record, task, view, workspace
|
|
5
|
+
from . import app, auth, builder, chart, exports, imports, portal, record, task, view, workspace
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def register_all_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -14,5 +14,6 @@ def register_all_commands(subparsers: argparse._SubParsersAction[argparse.Argume
|
|
|
14
14
|
chart.register(subparsers)
|
|
15
15
|
record.register(subparsers)
|
|
16
16
|
imports.register(subparsers)
|
|
17
|
+
exports.register(subparsers)
|
|
17
18
|
task.register(subparsers)
|
|
18
19
|
builder.register(subparsers)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from ..context import CliContext
|
|
6
|
+
from .common import load_list_arg
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
10
|
+
parser = subparsers.add_parser("export", help="导出")
|
|
11
|
+
export_subparsers = parser.add_subparsers(dest="export_command", required=True)
|
|
12
|
+
|
|
13
|
+
start = export_subparsers.add_parser("start", help="启动导出")
|
|
14
|
+
start.add_argument("--app-key", required=True)
|
|
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="是否同时导出流程日志")
|
|
21
|
+
start.set_defaults(handler=_handle_start, format_hint="export_start")
|
|
22
|
+
|
|
23
|
+
status = export_subparsers.add_parser("status", help="查询导出状态")
|
|
24
|
+
status.add_argument("--export-handle", required=True)
|
|
25
|
+
status.set_defaults(handler=_handle_status, format_hint="export_status")
|
|
26
|
+
|
|
27
|
+
get = export_subparsers.add_parser("get", help="获取导出结果")
|
|
28
|
+
get.add_argument("--export-handle", required=True)
|
|
29
|
+
get.add_argument("--download-to-path")
|
|
30
|
+
get.set_defaults(handler=_handle_get, format_hint="export_get")
|
|
31
|
+
|
|
32
|
+
direct = export_subparsers.add_parser("direct", help="直接导出并下载")
|
|
33
|
+
direct.add_argument("--app-key", required=True)
|
|
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="是否同时导出流程日志")
|
|
40
|
+
direct.add_argument("--download-to-path")
|
|
41
|
+
direct.add_argument("--wait-timeout-seconds", type=float)
|
|
42
|
+
direct.set_defaults(handler=_handle_direct, format_hint="export_direct")
|
|
43
|
+
|
|
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
|
+
|
|
59
|
+
def _handle_start(args: argparse.Namespace, context: CliContext) -> dict:
|
|
60
|
+
return context.exports.record_export_start(
|
|
61
|
+
profile=args.profile,
|
|
62
|
+
app_key=args.app_key,
|
|
63
|
+
view_id=args.view_id,
|
|
64
|
+
columns=_columns(args),
|
|
65
|
+
record_ids=_record_ids(args),
|
|
66
|
+
include_workflow_log=args.include_workflow_log,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _handle_status(args: argparse.Namespace, context: CliContext) -> dict:
|
|
71
|
+
return context.exports.record_export_status_get(
|
|
72
|
+
profile=args.profile,
|
|
73
|
+
export_handle=args.export_handle,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
78
|
+
return context.exports.record_export_get(
|
|
79
|
+
profile=args.profile,
|
|
80
|
+
export_handle=args.export_handle,
|
|
81
|
+
download_to_path=args.download_to_path,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _handle_direct(args: argparse.Namespace, context: CliContext) -> dict:
|
|
86
|
+
return context.exports.record_export_direct(
|
|
87
|
+
profile=args.profile,
|
|
88
|
+
app_key=args.app_key,
|
|
89
|
+
view_id=args.view_id,
|
|
90
|
+
columns=_columns(args),
|
|
91
|
+
record_ids=_record_ids(args),
|
|
92
|
+
include_workflow_log=args.include_workflow_log,
|
|
93
|
+
download_to_path=args.download_to_path,
|
|
94
|
+
wait_timeout_seconds=args.wait_timeout_seconds,
|
|
95
|
+
)
|
|
@@ -8,6 +8,7 @@ from ..tools.ai_builder_tools import AiBuilderTools
|
|
|
8
8
|
from ..tools.app_tools import AppTools
|
|
9
9
|
from ..tools.auth_tools import AuthTools
|
|
10
10
|
from ..tools.code_block_tools import CodeBlockTools
|
|
11
|
+
from ..tools.export_tools import ExportTools
|
|
11
12
|
from ..tools.feedback_tools import FeedbackTools
|
|
12
13
|
from ..tools.file_tools import FileTools
|
|
13
14
|
from ..tools.import_tools import ImportTools
|
|
@@ -29,6 +30,7 @@ class CliContext:
|
|
|
29
30
|
record: RecordTools
|
|
30
31
|
code_block: CodeBlockTools
|
|
31
32
|
imports: ImportTools
|
|
33
|
+
exports: ExportTools
|
|
32
34
|
task: TaskContextTools
|
|
33
35
|
files: FileTools
|
|
34
36
|
builder_feedback: FeedbackTools
|
|
@@ -52,6 +54,7 @@ def build_cli_context() -> CliContext:
|
|
|
52
54
|
record=RecordTools(sessions, backend),
|
|
53
55
|
code_block=CodeBlockTools(sessions, backend),
|
|
54
56
|
imports=ImportTools(sessions, backend),
|
|
57
|
+
exports=ExportTools(sessions, backend),
|
|
55
58
|
task=TaskContextTools(sessions, backend),
|
|
56
59
|
files=FileTools(sessions, backend),
|
|
57
60
|
builder_feedback=FeedbackTools(backend, mcp_side="App Builder MCP"),
|
|
@@ -412,6 +412,73 @@ def _format_import_status(result: dict[str, Any]) -> str:
|
|
|
412
412
|
return "\n".join(lines) + "\n"
|
|
413
413
|
|
|
414
414
|
|
|
415
|
+
def _format_export_common(result: dict[str, Any], *, title: str | None = None) -> str:
|
|
416
|
+
lines: list[str] = []
|
|
417
|
+
if title:
|
|
418
|
+
lines.append(title)
|
|
419
|
+
lines.extend(
|
|
420
|
+
[
|
|
421
|
+
f"Status: {result.get('status') or '-'}",
|
|
422
|
+
f"Export Handle: {result.get('export_handle') or '-'}",
|
|
423
|
+
f"App Key: {result.get('app_key') or '-'}",
|
|
424
|
+
f"View ID: {result.get('view_id') or '-'}",
|
|
425
|
+
f"Process Status: {result.get('process_status') or '-'}",
|
|
426
|
+
f"Rows: {result.get('num') if result.get('num') is not None else '-'}",
|
|
427
|
+
]
|
|
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}")
|
|
444
|
+
file_names = result.get("file_names") if isinstance(result.get("file_names"), list) else []
|
|
445
|
+
file_urls = result.get("file_urls") if isinstance(result.get("file_urls"), list) else []
|
|
446
|
+
if file_names or file_urls:
|
|
447
|
+
lines.append("Remote Files:")
|
|
448
|
+
max_items = max(len(file_names), len(file_urls))
|
|
449
|
+
for index in range(max_items):
|
|
450
|
+
name = file_names[index] if index < len(file_names) else "-"
|
|
451
|
+
url = file_urls[index] if index < len(file_urls) else "-"
|
|
452
|
+
lines.append(f"- {name}: {url}")
|
|
453
|
+
downloaded_files = result.get("downloaded_files") if isinstance(result.get("downloaded_files"), list) else []
|
|
454
|
+
if downloaded_files:
|
|
455
|
+
lines.append("Downloaded Files:")
|
|
456
|
+
for item in downloaded_files:
|
|
457
|
+
if isinstance(item, dict):
|
|
458
|
+
lines.append(f"- {item.get('file_name') or '-'}: {item.get('path') or '-'}")
|
|
459
|
+
else:
|
|
460
|
+
lines.append(f"- {item}")
|
|
461
|
+
_append_warnings(lines, result.get("warnings"))
|
|
462
|
+
_append_verification(lines, result.get("verification"))
|
|
463
|
+
return "\n".join(lines) + "\n"
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _format_export_start(result: dict[str, Any]) -> str:
|
|
467
|
+
return _format_export_common(result, title="Export Accepted")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _format_export_status(result: dict[str, Any]) -> str:
|
|
471
|
+
return _format_export_common(result, title="Export Status")
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _format_export_get(result: dict[str, Any]) -> str:
|
|
475
|
+
return _format_export_common(result, title="Export Result")
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _format_export_direct(result: dict[str, Any]) -> str:
|
|
479
|
+
return _format_export_common(result, title="Export Direct")
|
|
480
|
+
|
|
481
|
+
|
|
415
482
|
def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
416
483
|
lines = []
|
|
417
484
|
if "status" in result:
|
|
@@ -626,5 +693,9 @@ _FORMATTERS = {
|
|
|
626
693
|
"task_associated_report_detail_get": _format_task_associated_report_detail,
|
|
627
694
|
"import_verify": _format_import_verify,
|
|
628
695
|
"import_status": _format_import_status,
|
|
696
|
+
"export_start": _format_export_start,
|
|
697
|
+
"export_status": _format_export_status,
|
|
698
|
+
"export_get": _format_export_get,
|
|
699
|
+
"export_direct": _format_export_direct,
|
|
629
700
|
"builder_summary": _format_builder_summary,
|
|
630
701
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import timedelta
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .import_store import _JsonEntryStore, _store_dir
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExportJobStore(_JsonEntryStore):
|
|
10
|
+
def __init__(self, base_dir: Path | None = None, *, ttl_seconds: int = 24 * 3600) -> None:
|
|
11
|
+
super().__init__(
|
|
12
|
+
base_dir=base_dir or _store_dir("QINGFLOW_MCP_EXPORT_JOB_HOME", "export-jobs"),
|
|
13
|
+
ttl=timedelta(seconds=ttl_seconds),
|
|
14
|
+
)
|
|
@@ -91,6 +91,10 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
91
91
|
PublicToolSpec(USER_DOMAIN, "record_import_repair_local", ("record_import_repair_local",), ("import", "repair")),
|
|
92
92
|
PublicToolSpec(USER_DOMAIN, "record_import_start", ("record_import_start",), ("import", "start")),
|
|
93
93
|
PublicToolSpec(USER_DOMAIN, "record_import_status_get", ("record_import_status_get",), ("import", "status")),
|
|
94
|
+
PublicToolSpec(USER_DOMAIN, "record_export_start", ("record_export_start",), ("export", "start"), cli_show_effective_context=True),
|
|
95
|
+
PublicToolSpec(USER_DOMAIN, "record_export_status_get", ("record_export_status_get",), ("export", "status"), cli_show_effective_context=True),
|
|
96
|
+
PublicToolSpec(USER_DOMAIN, "record_export_get", ("record_export_get",), ("export", "get"), cli_show_effective_context=True),
|
|
97
|
+
PublicToolSpec(USER_DOMAIN, "record_export_direct", ("record_export_direct",), ("export", "direct"), cli_show_effective_context=True),
|
|
94
98
|
PublicToolSpec(USER_DOMAIN, "record_code_block_run", ("record_code_block_run",), ("record", "code-block-run"), cli_show_effective_context=True, cli_context_write=True),
|
|
95
99
|
PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
|
|
96
100
|
PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
|
|
@@ -674,6 +674,10 @@ def _trim_import_status_payload(payload: JSONObject) -> None:
|
|
|
674
674
|
payload.pop(key, None)
|
|
675
675
|
|
|
676
676
|
|
|
677
|
+
def _trim_export_payload(payload: JSONObject) -> None:
|
|
678
|
+
payload.pop("backend_export_id", None)
|
|
679
|
+
|
|
680
|
+
|
|
677
681
|
def _summarize_import_issues(issues: list[Any]) -> dict[str, Any]:
|
|
678
682
|
total = 0
|
|
679
683
|
error_count = 0
|
|
@@ -756,6 +760,16 @@ _register_policy(
|
|
|
756
760
|
),
|
|
757
761
|
_trim_import_schema,
|
|
758
762
|
)
|
|
763
|
+
_register_policy(
|
|
764
|
+
(USER_DOMAIN,),
|
|
765
|
+
(
|
|
766
|
+
"record_export_start",
|
|
767
|
+
"record_export_status_get",
|
|
768
|
+
"record_export_get",
|
|
769
|
+
"record_export_direct",
|
|
770
|
+
),
|
|
771
|
+
_trim_export_payload,
|
|
772
|
+
)
|
|
759
773
|
_register_policy(
|
|
760
774
|
(USER_DOMAIN,),
|
|
761
775
|
(
|
|
@@ -10,6 +10,7 @@ from .tools.app_tools import AppTools
|
|
|
10
10
|
from .tools.auth_tools import AuthTools
|
|
11
11
|
from .tools.code_block_tools import CodeBlockTools
|
|
12
12
|
from .tools.feedback_tools import FeedbackTools
|
|
13
|
+
from .tools.export_tools import ExportTools
|
|
13
14
|
from .tools.file_tools import FileTools
|
|
14
15
|
from .tools.import_tools import ImportTools
|
|
15
16
|
from .tools.package_tools import PackageTools
|
|
@@ -50,6 +51,7 @@ All resource tools operate with the logged-in user's Qingflow permissions.
|
|
|
50
51
|
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
51
52
|
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
52
53
|
If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
|
|
54
|
+
`view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
|
|
53
55
|
|
|
54
56
|
## Schema-First Rule
|
|
55
57
|
|
|
@@ -147,6 +149,20 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
147
149
|
- Do not modify user-uploaded files unless the user explicitly authorizes repair.
|
|
148
150
|
- If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
|
|
149
151
|
|
|
152
|
+
## Export Path
|
|
153
|
+
|
|
154
|
+
`view_get -> record_export_start -> record_export_status_get -> record_export_get`
|
|
155
|
+
|
|
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
|
+
- 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.
|
|
165
|
+
|
|
150
166
|
## Task Workflow Path
|
|
151
167
|
|
|
152
168
|
`task_list -> task_get -> task_action_execute`
|
|
@@ -190,6 +206,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
190
206
|
WorkspaceTools(sessions, backend).register(server)
|
|
191
207
|
FileTools(sessions, backend).register(server)
|
|
192
208
|
ImportTools(sessions, backend).register(server)
|
|
209
|
+
ExportTools(sessions, backend).register(server)
|
|
193
210
|
CodeBlockTools(sessions, backend).register(server)
|
|
194
211
|
TaskContextTools(sessions, backend).register(server)
|
|
195
212
|
RoleTools(sessions, backend).register(server)
|
|
@@ -13,6 +13,7 @@ from .tools.app_tools import AppTools
|
|
|
13
13
|
from .tools.auth_tools import AuthTools
|
|
14
14
|
from .tools.code_block_tools import CodeBlockTools
|
|
15
15
|
from .tools.directory_tools import DirectoryTools
|
|
16
|
+
from .tools.export_tools import ExportTools
|
|
16
17
|
from .tools.feedback_tools import FeedbackTools
|
|
17
18
|
from .tools.file_tools import FileTools
|
|
18
19
|
from .tools.import_tools import ImportTools
|
|
@@ -33,6 +34,7 @@ def build_user_server() -> FastMCP:
|
|
|
33
34
|
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
34
35
|
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
35
36
|
If an accessible view has `analysis_supported=false`, do not use it for `record_list` or `record_analyze`. `boardView` and `ganttView` are special UI views, not list/analyze targets.
|
|
37
|
+
`view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
|
|
36
38
|
|
|
37
39
|
## Shared Helper
|
|
38
40
|
|
|
@@ -142,6 +144,20 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
142
144
|
- Do not modify user-uploaded files unless the user explicitly authorizes repair.
|
|
143
145
|
- If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
|
|
144
146
|
|
|
147
|
+
## Export Path
|
|
148
|
+
|
|
149
|
+
`view_get -> record_export_start -> record_export_status_get -> record_export_get`
|
|
150
|
+
|
|
151
|
+
- `record_export_direct` is the one-shot path that starts export, waits, downloads locally, and still returns remote download links.
|
|
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.
|
|
160
|
+
|
|
145
161
|
## Task Workflow Path
|
|
146
162
|
|
|
147
163
|
`task_list -> task_get -> task_action_execute`
|
|
@@ -188,6 +204,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
188
204
|
workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
189
205
|
file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
190
206
|
imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
207
|
+
exports = wrap_trimmed_methods(ExportTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
191
208
|
resources = wrap_trimmed_methods(ResourceReadTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
192
209
|
feedback = FeedbackTools(backend, mcp_side="App User MCP")
|
|
193
210
|
code_block_tools = wrap_trimmed_methods(CodeBlockTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
@@ -218,6 +235,65 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
218
235
|
def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict:
|
|
219
236
|
return auth.auth_logout(profile=profile, forget_persisted=forget_persisted)
|
|
220
237
|
|
|
238
|
+
@server.tool()
|
|
239
|
+
def record_export_start(
|
|
240
|
+
profile: str = DEFAULT_PROFILE,
|
|
241
|
+
app_key: str = "",
|
|
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,
|
|
246
|
+
) -> dict:
|
|
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
|
+
)
|
|
255
|
+
|
|
256
|
+
@server.tool()
|
|
257
|
+
def record_export_status_get(
|
|
258
|
+
profile: str = DEFAULT_PROFILE,
|
|
259
|
+
export_handle: str = "",
|
|
260
|
+
) -> dict:
|
|
261
|
+
return exports.record_export_status_get(profile=profile, export_handle=export_handle)
|
|
262
|
+
|
|
263
|
+
@server.tool()
|
|
264
|
+
def record_export_get(
|
|
265
|
+
profile: str = DEFAULT_PROFILE,
|
|
266
|
+
export_handle: str = "",
|
|
267
|
+
download_to_path: str | None = None,
|
|
268
|
+
) -> dict:
|
|
269
|
+
return exports.record_export_get(
|
|
270
|
+
profile=profile,
|
|
271
|
+
export_handle=export_handle,
|
|
272
|
+
download_to_path=download_to_path,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
@server.tool()
|
|
276
|
+
def record_export_direct(
|
|
277
|
+
profile: str = DEFAULT_PROFILE,
|
|
278
|
+
app_key: str = "",
|
|
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,
|
|
283
|
+
download_to_path: str | None = None,
|
|
284
|
+
wait_timeout_seconds: float | None = None,
|
|
285
|
+
) -> dict:
|
|
286
|
+
return exports.record_export_direct(
|
|
287
|
+
profile=profile,
|
|
288
|
+
app_key=app_key,
|
|
289
|
+
view_id=view_id,
|
|
290
|
+
columns=columns or [],
|
|
291
|
+
record_ids=record_ids or [],
|
|
292
|
+
include_workflow_log=include_workflow_log,
|
|
293
|
+
download_to_path=download_to_path,
|
|
294
|
+
wait_timeout_seconds=wait_timeout_seconds,
|
|
295
|
+
)
|
|
296
|
+
|
|
221
297
|
@server.tool()
|
|
222
298
|
def workspace_list(
|
|
223
299
|
profile: str = DEFAULT_PROFILE,
|