@qingflow-tech/qingflow-app-user-mcp 1.0.2 → 1.0.4

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 (56) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +9 -3
  3. package/npm/lib/runtime.mjs +10 -3
  4. package/package.json +1 -1
  5. package/pyproject.toml +1 -1
  6. package/skills/qingflow-app-user/SKILL.md +21 -12
  7. package/skills/qingflow-app-user/references/data-gotchas.md +1 -1
  8. package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
  9. package/skills/qingflow-app-user/references/record-patterns.md +1 -1
  10. package/skills/qingflow-record-analysis/SKILL.md +44 -2
  11. package/skills/qingflow-record-insert/SKILL.md +3 -0
  12. package/skills/qingflow-record-update/SKILL.md +3 -0
  13. package/skills/qingflow-task-ops/SKILL.md +31 -10
  14. package/src/qingflow_mcp/__init__.py +33 -1
  15. package/src/qingflow_mcp/backend_client.py +109 -0
  16. package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
  17. package/src/qingflow_mcp/builder_facade/models.py +58 -9
  18. package/src/qingflow_mcp/builder_facade/service.py +1711 -240
  19. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  20. package/src/qingflow_mcp/cli/commands/app.py +47 -1
  21. package/src/qingflow_mcp/cli/commands/auth.py +63 -0
  22. package/src/qingflow_mcp/cli/commands/builder.py +11 -3
  23. package/src/qingflow_mcp/cli/commands/exports.py +111 -0
  24. package/src/qingflow_mcp/cli/commands/record.py +5 -5
  25. package/src/qingflow_mcp/cli/commands/task.py +701 -27
  26. package/src/qingflow_mcp/cli/commands/workspace.py +84 -0
  27. package/src/qingflow_mcp/cli/context.py +3 -0
  28. package/src/qingflow_mcp/cli/formatters.py +424 -50
  29. package/src/qingflow_mcp/cli/interaction.py +72 -0
  30. package/src/qingflow_mcp/cli/main.py +11 -1
  31. package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
  32. package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
  33. package/src/qingflow_mcp/config.py +1 -1
  34. package/src/qingflow_mcp/errors.py +4 -4
  35. package/src/qingflow_mcp/export_store.py +14 -0
  36. package/src/qingflow_mcp/id_utils.py +49 -0
  37. package/src/qingflow_mcp/public_surface.py +16 -1
  38. package/src/qingflow_mcp/response_trim.py +394 -9
  39. package/src/qingflow_mcp/server.py +26 -0
  40. package/src/qingflow_mcp/server_app_builder.py +15 -1
  41. package/src/qingflow_mcp/server_app_user.py +113 -0
  42. package/src/qingflow_mcp/session_store.py +126 -21
  43. package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
  44. package/src/qingflow_mcp/solution/executor.py +2 -2
  45. package/src/qingflow_mcp/tools/ai_builder_tools.py +107 -34
  46. package/src/qingflow_mcp/tools/app_tools.py +1 -0
  47. package/src/qingflow_mcp/tools/auth_tools.py +243 -9
  48. package/src/qingflow_mcp/tools/base.py +6 -2
  49. package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
  50. package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
  51. package/src/qingflow_mcp/tools/export_tools.py +1565 -0
  52. package/src/qingflow_mcp/tools/import_tools.py +78 -4
  53. package/src/qingflow_mcp/tools/record_tools.py +551 -165
  54. package/src/qingflow_mcp/tools/resource_read_tools.py +154 -33
  55. package/src/qingflow_mcp/tools/task_context_tools.py +917 -141
  56. package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
@@ -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)
@@ -3,6 +3,9 @@ from __future__ import annotations
3
3
  import argparse
4
4
 
5
5
  from ..context import CliContext
6
+ from ..interaction import cancelled_result, resolve_interactive_selection
7
+ from ..terminal_ui import SelectionOption
8
+ from .common import raise_config_error
6
9
 
7
10
 
8
11
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -19,7 +22,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
19
22
  search.set_defaults(handler=_handle_search, format_hint="app_search")
20
23
 
21
24
  get = app_subparsers.add_parser("get", help="读取应用可访问视图与导入能力")
22
- get.add_argument("--app-key", required=True)
25
+ get.add_argument("--app-key", help="不传时在交互终端中选择应用")
23
26
  get.set_defaults(handler=_handle_get, format_hint="app_get")
24
27
 
25
28
 
@@ -37,4 +40,47 @@ def _handle_search(args: argparse.Namespace, context: CliContext) -> dict:
37
40
 
38
41
 
39
42
  def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
43
+ if not (args.app_key or "").strip():
44
+ selection = _choose_app_interactively(args, context)
45
+ if selection.status == "unavailable":
46
+ raise_config_error(
47
+ "app get requires --app-key, or an interactive terminal to choose an app",
48
+ fix_hint="Run `app list` to inspect visible apps, or retry with `--app-key APP_KEY`.",
49
+ )
50
+ if selection.status == "empty":
51
+ raise_config_error(
52
+ selection.message or "app get could not open a selector because no visible apps were returned.",
53
+ fix_hint="Run `app list` to confirm visible apps, or retry with `--app-key APP_KEY`.",
54
+ )
55
+ if selection.status == "cancelled":
56
+ return cancelled_result(selection.message or "已取消")
57
+ args.app_key = str(selection.value or "")
40
58
  return context.app.app_get(profile=args.profile, app_key=args.app_key)
59
+
60
+
61
+ def _choose_app_interactively(args: argparse.Namespace, context: CliContext):
62
+ def load_options() -> list[SelectionOption[str]]:
63
+ result = context.app.app_list(profile=args.profile)
64
+ items = result.get("items") if isinstance(result, dict) and isinstance(result.get("items"), list) else []
65
+ options: list[SelectionOption[str]] = []
66
+ for item in items:
67
+ if not isinstance(item, dict):
68
+ continue
69
+ app_key = str(item.get("app_key") or "").strip()
70
+ if not app_key:
71
+ continue
72
+ app_name = str(item.get("app_name") or app_key).strip() or app_key
73
+ package_name = str(item.get("package_name") or "").strip()
74
+ hint = f"app_key={app_key}"
75
+ if package_name:
76
+ hint += f" · package={package_name}"
77
+ options.append(SelectionOption(value=app_key, label=app_name, hint=hint))
78
+ return options
79
+
80
+ return resolve_interactive_selection(
81
+ args,
82
+ title="选择应用",
83
+ unavailable_message="app get requires --app-key, or an interactive terminal to choose an app",
84
+ empty_message="app get could not open a selector because no visible apps were returned.",
85
+ load_options=load_options,
86
+ )
@@ -1,8 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import getpass
5
+ import sys
4
6
 
7
+ from ...errors import QingflowApiError
5
8
  from ..context import CliContext
9
+ from ..qingflow_login import login_with_qingflow_password
6
10
  from .common import read_secret_arg
7
11
 
8
12
 
@@ -10,6 +14,15 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
10
14
  parser = subparsers.add_parser("auth", help="认证与会话")
11
15
  auth_subparsers = parser.add_subparsers(dest="auth_command", required=True)
12
16
 
17
+ login = auth_subparsers.add_parser("login", help="登录 CLI:轻流账号密码交互")
18
+ login.add_argument("--base-url")
19
+ login.add_argument("--qf-version")
20
+ login.add_argument("--email", help="轻流账号邮箱;不传时在交互终端提示输入")
21
+ login.add_argument("--password", help="轻流账号密码;建议仅用于本地调试,脚本请优先使用 --password-stdin")
22
+ login.add_argument("--password-stdin", action="store_true", help="从标准输入读取轻流账号密码")
23
+ login.add_argument("--persist", action=argparse.BooleanOptionalAction, default=True)
24
+ login.set_defaults(handler=_handle_login, format_hint="auth_whoami")
25
+
13
26
  use_credential = auth_subparsers.add_parser("use-credential", help="直接注入 credential")
14
27
  use_credential.add_argument("--base-url")
15
28
  use_credential.add_argument("--qf-version")
@@ -26,6 +39,56 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
26
39
  logout.set_defaults(handler=_handle_logout, format_hint="")
27
40
 
28
41
 
42
+ def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
43
+ email = _resolve_login_email(args)
44
+ password = _resolve_login_password(args)
45
+ login_result = login_with_qingflow_password(
46
+ base_url=args.base_url,
47
+ email=email,
48
+ password=password,
49
+ )
50
+ result = context.auth.auth_use_token(
51
+ profile=args.profile,
52
+ base_url=args.base_url,
53
+ qf_version=args.qf_version,
54
+ token=login_result.token,
55
+ login_token=login_result.login_token,
56
+ user_info=login_result.user_info,
57
+ persist=bool(args.persist),
58
+ )
59
+ warnings = list(result.get("warnings") or []) if isinstance(result, dict) else []
60
+ if isinstance(result, dict):
61
+ result["cli_auth"] = {"flow": login_result.flow}
62
+ if warnings:
63
+ result["warnings"] = warnings
64
+ return result
65
+
66
+
67
+ def _resolve_login_email(args: argparse.Namespace) -> str:
68
+ email = str(args.email or "").strip()
69
+ if email:
70
+ return email
71
+ if sys.stdin.isatty():
72
+ return input("Qingflow email: ").strip()
73
+ raise QingflowApiError.config_error(
74
+ "qingflow auth login needs an interactive terminal, or pass --email with --password-stdin."
75
+ )
76
+
77
+
78
+ def _resolve_login_password(args: argparse.Namespace) -> str:
79
+ if args.password or bool(args.password_stdin):
80
+ return read_secret_arg(
81
+ args.password,
82
+ stdin_enabled=bool(args.password_stdin),
83
+ label="password",
84
+ )
85
+ if sys.stdin.isatty():
86
+ return getpass.getpass("Qingflow password: ")
87
+ raise QingflowApiError.config_error(
88
+ "qingflow auth login needs an interactive terminal or --password-stdin."
89
+ )
90
+
91
+
29
92
  def _handle_use_credential(args: argparse.Namespace, context: CliContext) -> dict:
30
93
  credential = (
31
94
  read_secret_arg(args.credential, stdin_enabled=bool(args.credential_stdin), label="credential")
@@ -101,7 +101,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
101
101
  app_release_lock.add_argument("--lock-owner-name", required=True)
102
102
  app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary")
103
103
 
104
- app_get = app_subparsers.add_parser("get", help="读取应用配置")
104
+ app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
105
105
  app_get.add_argument(
106
106
  "builder_app_get_section",
107
107
  nargs="?",
@@ -120,6 +120,9 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
120
120
  button = builder_subparsers.add_parser("button", help="自定义按钮")
121
121
  button_subparsers = button.add_subparsers(dest="builder_button_command", required=True)
122
122
 
123
+ button_catalog = button_subparsers.add_parser("catalog", help="读取按钮样式目录")
124
+ button_catalog.set_defaults(handler=_handle_button_catalog, format_hint="builder_summary")
125
+
123
126
  button_list = button_subparsers.add_parser("list", help="列出自定义按钮")
124
127
  button_list.add_argument("--app-key", required=True)
125
128
  button_list.set_defaults(handler=_handle_button_list, format_hint="builder_summary")
@@ -160,7 +163,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
160
163
  portal_apply.add_argument("--dash-name", default="")
161
164
  portal_apply.add_argument("--package-id", type=int)
162
165
  portal_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
163
- portal_apply.add_argument("--sections-file", required=True)
166
+ portal_apply.add_argument("--sections-file")
164
167
  portal_apply.add_argument("--visibility-file")
165
168
  portal_apply.add_argument("--auth-file")
166
169
  portal_apply.add_argument("--icon")
@@ -362,6 +365,10 @@ def _handle_button_list(args: argparse.Namespace, context: CliContext) -> dict:
362
365
  return context.builder.app_custom_button_list(profile=args.profile, app_key=args.app_key)
363
366
 
364
367
 
368
+ def _handle_button_catalog(args: argparse.Namespace, context: CliContext) -> dict:
369
+ return context.builder.button_style_catalog_get(profile=args.profile)
370
+
371
+
365
372
  def _handle_button_get(args: argparse.Namespace, context: CliContext) -> dict:
366
373
  return context.builder.app_custom_button_get(profile=args.profile, app_key=args.app_key, button_id=args.button_id)
367
374
 
@@ -513,13 +520,14 @@ def _handle_portal_apply(args: argparse.Namespace, context: CliContext) -> dict:
513
520
  "portal apply requires either --dash-key, or --package-id together with --dash-name.",
514
521
  fix_hint="Use `--dash-key` for an existing portal. For create mode, pass `--package-id --dash-name`.",
515
522
  )
523
+ sections = [] if not args.sections_file else require_list_arg(args.sections_file, option_name="--sections-file")
516
524
  return context.builder.portal_apply(
517
525
  profile=args.profile,
518
526
  dash_key=args.dash_key,
519
527
  dash_name=args.dash_name,
520
528
  package_id=args.package_id,
521
529
  publish=bool(args.publish),
522
- sections=require_list_arg(args.sections_file, option_name="--sections-file"),
530
+ sections=sections,
523
531
  visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
524
532
  auth=load_object_arg(args.auth_file, option_name="--auth-file"),
525
533
  icon=args.icon,
@@ -0,0 +1,111 @@
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("--where-file", help="JSON/YAML list,内容与 record list 的 where DSL 一致;内部先查命中 record_id 再走原生导出")
19
+ start.add_argument("--order-by-file", help="JSON/YAML list,内容与 record list 的 order_by DSL 一致;内部查询和导出记录顺序保持一致")
20
+ start.add_argument("--record-id", dest="record_ids", action="append", default=[], help="只导出这些 record_id;不传时导出当前视图全部数据")
21
+ start.add_argument("--record-ids-file", help="JSON/YAML list,内容与 --record-id 语义一致")
22
+ start.add_argument("--include-workflow-log", action=argparse.BooleanOptionalAction, default=False, help="是否同时导出流程日志")
23
+ start.set_defaults(handler=_handle_start, format_hint="export_start")
24
+
25
+ status = export_subparsers.add_parser("status", help="查询导出状态")
26
+ status.add_argument("--export-handle", required=True)
27
+ status.set_defaults(handler=_handle_status, format_hint="export_status")
28
+
29
+ get = export_subparsers.add_parser("get", help="获取导出结果")
30
+ get.add_argument("--export-handle", required=True)
31
+ get.add_argument("--download-to-path")
32
+ get.set_defaults(handler=_handle_get, format_hint="export_get")
33
+
34
+ direct = export_subparsers.add_parser("direct", help="直接导出并下载")
35
+ direct.add_argument("--app-key", required=True)
36
+ direct.add_argument("--view-id", default="system:all")
37
+ direct.add_argument("--column", dest="columns", action="append", type=int, default=[], help="只导出这些 field_id;不传时导出当前视图全部字段")
38
+ direct.add_argument("--columns-file", help="JSON/YAML list,内容与 --column 语义一致")
39
+ direct.add_argument("--where-file", help="JSON/YAML list,内容与 record list 的 where DSL 一致;内部先查命中 record_id 再走原生导出")
40
+ direct.add_argument("--order-by-file", help="JSON/YAML list,内容与 record list 的 order_by DSL 一致;内部查询和导出记录顺序保持一致")
41
+ direct.add_argument("--record-id", dest="record_ids", action="append", default=[], help="只导出这些 record_id;不传时导出当前视图全部数据")
42
+ direct.add_argument("--record-ids-file", help="JSON/YAML list,内容与 --record-id 语义一致")
43
+ direct.add_argument("--include-workflow-log", action=argparse.BooleanOptionalAction, default=False, help="是否同时导出流程日志")
44
+ direct.add_argument("--download-to-path")
45
+ direct.add_argument("--wait-timeout-seconds", type=float)
46
+ direct.set_defaults(handler=_handle_direct, format_hint="export_direct")
47
+
48
+
49
+ def _columns(args: argparse.Namespace) -> list[int | dict]:
50
+ columns: list[int | dict] = list(args.columns or [])
51
+ if args.columns_file:
52
+ columns.extend(load_list_arg(args.columns_file, option_name="--columns-file"))
53
+ return columns
54
+
55
+
56
+ def _record_ids(args: argparse.Namespace) -> list[str | int]:
57
+ record_ids: list[str | int] = list(args.record_ids or [])
58
+ if args.record_ids_file:
59
+ record_ids.extend(load_list_arg(args.record_ids_file, option_name="--record-ids-file"))
60
+ return record_ids
61
+
62
+
63
+ def _where(args: argparse.Namespace) -> list[dict]:
64
+ return load_list_arg(args.where_file, option_name="--where-file") if args.where_file else []
65
+
66
+
67
+ def _order_by(args: argparse.Namespace) -> list[dict]:
68
+ return load_list_arg(args.order_by_file, option_name="--order-by-file") if args.order_by_file else []
69
+
70
+
71
+ def _handle_start(args: argparse.Namespace, context: CliContext) -> dict:
72
+ return context.exports.record_export_start(
73
+ profile=args.profile,
74
+ app_key=args.app_key,
75
+ view_id=args.view_id,
76
+ columns=_columns(args),
77
+ where=_where(args),
78
+ order_by=_order_by(args),
79
+ record_ids=_record_ids(args),
80
+ include_workflow_log=args.include_workflow_log,
81
+ )
82
+
83
+
84
+ def _handle_status(args: argparse.Namespace, context: CliContext) -> dict:
85
+ return context.exports.record_export_status_get(
86
+ profile=args.profile,
87
+ export_handle=args.export_handle,
88
+ )
89
+
90
+
91
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
92
+ return context.exports.record_export_get(
93
+ profile=args.profile,
94
+ export_handle=args.export_handle,
95
+ download_to_path=args.download_to_path,
96
+ )
97
+
98
+
99
+ def _handle_direct(args: argparse.Namespace, context: CliContext) -> dict:
100
+ return context.exports.record_export_direct(
101
+ profile=args.profile,
102
+ app_key=args.app_key,
103
+ view_id=args.view_id,
104
+ columns=_columns(args),
105
+ where=_where(args),
106
+ order_by=_order_by(args),
107
+ record_ids=_record_ids(args),
108
+ include_workflow_log=args.include_workflow_log,
109
+ download_to_path=args.download_to_path,
110
+ wait_timeout_seconds=args.wait_timeout_seconds,
111
+ )
@@ -32,7 +32,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
32
32
 
33
33
  schema_update = schema_subparsers.add_parser("update", help="读取更新记录表结构")
34
34
  schema_update.add_argument("--app-key", required=True)
35
- schema_update.add_argument("--record-id", required=True, type=int)
35
+ schema_update.add_argument("--record-id", required=True)
36
36
  schema_update.set_defaults(handler=_handle_schema_update, format_hint="")
37
37
 
38
38
  schema_import = schema_subparsers.add_parser("import", help="读取导入表结构")
@@ -59,7 +59,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
59
59
 
60
60
  get = record_subparsers.add_parser("get", help="读取单条记录")
61
61
  get.add_argument("--app-key", required=True)
62
- get.add_argument("--record-id", required=True, type=int)
62
+ get.add_argument("--record-id", required=True)
63
63
  get.add_argument("--column", dest="columns", action="append", type=int, default=[])
64
64
  get.add_argument("--columns-file")
65
65
  get.add_argument("--view-id")
@@ -73,7 +73,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
73
73
 
74
74
  update = record_subparsers.add_parser("update", help="更新记录")
75
75
  update.add_argument("--app-key", required=True)
76
- update.add_argument("--record-id", type=int)
76
+ update.add_argument("--record-id")
77
77
  update.add_argument("--fields-file")
78
78
  update.add_argument("--items-file")
79
79
  update.add_argument("--dry-run", action=argparse.BooleanOptionalAction, default=False)
@@ -82,7 +82,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
82
82
 
83
83
  delete = record_subparsers.add_parser("delete", help="删除记录")
84
84
  delete.add_argument("--app-key", required=True)
85
- delete.add_argument("--record-id", type=int)
85
+ delete.add_argument("--record-id")
86
86
  delete.add_argument("--record-ids-file")
87
87
  delete.set_defaults(handler=_handle_delete, format_hint="")
88
88
 
@@ -102,7 +102,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
102
102
 
103
103
  code_block = record_subparsers.add_parser("code-block-run", help="执行代码块字段")
104
104
  code_block.add_argument("--app-key", required=True)
105
- code_block.add_argument("--record-id", required=True, type=int)
105
+ code_block.add_argument("--record-id", required=True)
106
106
  code_block.add_argument("--code-block-field", required=True)
107
107
  code_block.add_argument("--role", type=int, default=1)
108
108
  code_block.add_argument("--workflow-node-id", type=int)