@qingflow-tech/qingflow-app-user-mcp 1.0.0

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 (109) hide show
  1. package/README.md +37 -0
  2. package/docs/local-agent-install.md +332 -0
  3. package/entry_point.py +13 -0
  4. package/npm/bin/qingflow-app-user-mcp.mjs +7 -0
  5. package/npm/lib/runtime.mjs +339 -0
  6. package/npm/scripts/postinstall.mjs +16 -0
  7. package/package.json +34 -0
  8. package/pyproject.toml +67 -0
  9. package/qingflow-app-user-mcp +15 -0
  10. package/skills/qingflow-app-user/SKILL.md +79 -0
  11. package/skills/qingflow-app-user/agents/openai.yaml +4 -0
  12. package/skills/qingflow-app-user/references/data-gotchas.md +29 -0
  13. package/skills/qingflow-app-user/references/environments.md +63 -0
  14. package/skills/qingflow-app-user/references/record-patterns.md +48 -0
  15. package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
  16. package/skills/qingflow-record-analysis/SKILL.md +158 -0
  17. package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
  18. package/skills/qingflow-record-analysis/references/analysis-gotchas.md +145 -0
  19. package/skills/qingflow-record-analysis/references/analysis-patterns.md +125 -0
  20. package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
  21. package/skills/qingflow-record-analysis/references/dsl-templates.md +93 -0
  22. package/skills/qingflow-record-delete/SKILL.md +29 -0
  23. package/skills/qingflow-record-import/SKILL.md +31 -0
  24. package/skills/qingflow-record-insert/SKILL.md +58 -0
  25. package/skills/qingflow-record-update/SKILL.md +42 -0
  26. package/skills/qingflow-task-ops/SKILL.md +123 -0
  27. package/skills/qingflow-task-ops/agents/openai.yaml +4 -0
  28. package/skills/qingflow-task-ops/references/environments.md +44 -0
  29. package/skills/qingflow-task-ops/references/workflow-usage.md +27 -0
  30. package/src/qingflow_mcp/__init__.py +5 -0
  31. package/src/qingflow_mcp/__main__.py +5 -0
  32. package/src/qingflow_mcp/backend_client.py +649 -0
  33. package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
  34. package/src/qingflow_mcp/builder_facade/models.py +1836 -0
  35. package/src/qingflow_mcp/builder_facade/service.py +15044 -0
  36. package/src/qingflow_mcp/cli/__init__.py +1 -0
  37. package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
  38. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  39. package/src/qingflow_mcp/cli/commands/auth.py +44 -0
  40. package/src/qingflow_mcp/cli/commands/builder.py +538 -0
  41. package/src/qingflow_mcp/cli/commands/chart.py +18 -0
  42. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  43. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  44. package/src/qingflow_mcp/cli/commands/portal.py +25 -0
  45. package/src/qingflow_mcp/cli/commands/record.py +331 -0
  46. package/src/qingflow_mcp/cli/commands/repo.py +80 -0
  47. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  48. package/src/qingflow_mcp/cli/commands/view.py +18 -0
  49. package/src/qingflow_mcp/cli/commands/workspace.py +25 -0
  50. package/src/qingflow_mcp/cli/context.py +60 -0
  51. package/src/qingflow_mcp/cli/formatters.py +334 -0
  52. package/src/qingflow_mcp/cli/json_io.py +50 -0
  53. package/src/qingflow_mcp/cli/main.py +178 -0
  54. package/src/qingflow_mcp/config.py +513 -0
  55. package/src/qingflow_mcp/errors.py +66 -0
  56. package/src/qingflow_mcp/import_store.py +121 -0
  57. package/src/qingflow_mcp/json_types.py +18 -0
  58. package/src/qingflow_mcp/list_type_labels.py +76 -0
  59. package/src/qingflow_mcp/public_surface.py +233 -0
  60. package/src/qingflow_mcp/repository_store.py +71 -0
  61. package/src/qingflow_mcp/response_trim.py +470 -0
  62. package/src/qingflow_mcp/server.py +212 -0
  63. package/src/qingflow_mcp/server_app_builder.py +533 -0
  64. package/src/qingflow_mcp/server_app_user.py +362 -0
  65. package/src/qingflow_mcp/session_store.py +302 -0
  66. package/src/qingflow_mcp/solution/__init__.py +6 -0
  67. package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
  68. package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
  69. package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
  70. package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
  71. package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
  72. package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
  73. package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
  74. package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
  75. package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
  76. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
  77. package/src/qingflow_mcp/solution/design_session.py +222 -0
  78. package/src/qingflow_mcp/solution/design_store.py +100 -0
  79. package/src/qingflow_mcp/solution/executor.py +2398 -0
  80. package/src/qingflow_mcp/solution/normalizer.py +23 -0
  81. package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
  82. package/src/qingflow_mcp/solution/run_store.py +244 -0
  83. package/src/qingflow_mcp/solution/spec_models.py +855 -0
  84. package/src/qingflow_mcp/tools/__init__.py +1 -0
  85. package/src/qingflow_mcp/tools/ai_builder_tools.py +3419 -0
  86. package/src/qingflow_mcp/tools/app_tools.py +925 -0
  87. package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
  88. package/src/qingflow_mcp/tools/auth_tools.py +875 -0
  89. package/src/qingflow_mcp/tools/base.py +388 -0
  90. package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
  91. package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
  92. package/src/qingflow_mcp/tools/directory_tools.py +675 -0
  93. package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
  94. package/src/qingflow_mcp/tools/file_tools.py +409 -0
  95. package/src/qingflow_mcp/tools/import_tools.py +2189 -0
  96. package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
  97. package/src/qingflow_mcp/tools/package_tools.py +326 -0
  98. package/src/qingflow_mcp/tools/portal_tools.py +158 -0
  99. package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
  100. package/src/qingflow_mcp/tools/record_tools.py +14037 -0
  101. package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
  102. package/src/qingflow_mcp/tools/resource_read_tools.py +421 -0
  103. package/src/qingflow_mcp/tools/role_tools.py +112 -0
  104. package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
  105. package/src/qingflow_mcp/tools/task_context_tools.py +2228 -0
  106. package/src/qingflow_mcp/tools/task_tools.py +890 -0
  107. package/src/qingflow_mcp/tools/view_tools.py +335 -0
  108. package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
  109. package/src/qingflow_mcp/tools/workspace_tools.py +125 -0
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from ..context import CliContext
6
+ from .common import load_object_arg
7
+
8
+
9
+ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
10
+ parser = subparsers.add_parser("task", help="待办与流程上下文")
11
+ task_subparsers = parser.add_subparsers(dest="task_command", required=True)
12
+
13
+ list_parser = task_subparsers.add_parser("list", help="列出待办")
14
+ list_parser.add_argument("--task-box", default="todo")
15
+ list_parser.add_argument("--flow-status", default="all")
16
+ list_parser.add_argument("--app-key")
17
+ list_parser.add_argument("--workflow-node-id", type=int)
18
+ list_parser.add_argument("--query")
19
+ list_parser.add_argument("--page", type=int, default=1)
20
+ list_parser.add_argument("--page-size", type=int, default=20)
21
+ list_parser.set_defaults(handler=_handle_list, format_hint="task_list")
22
+
23
+ get = task_subparsers.add_parser("get", help="读取待办详情")
24
+ get.add_argument("--app-key", required=True)
25
+ get.add_argument("--record-id", required=True, type=int)
26
+ get.add_argument("--workflow-node-id", required=True, type=int)
27
+ get.add_argument("--include-candidates", action=argparse.BooleanOptionalAction, default=True)
28
+ get.add_argument("--include-associated-reports", action=argparse.BooleanOptionalAction, default=True)
29
+ get.set_defaults(handler=_handle_get, format_hint="task_get")
30
+
31
+ action = task_subparsers.add_parser("action", help="执行待办动作")
32
+ action.add_argument("--app-key", required=True)
33
+ action.add_argument("--record-id", required=True, type=int)
34
+ action.add_argument("--workflow-node-id", required=True, type=int)
35
+ action.add_argument("--action", required=True)
36
+ action.add_argument("--payload-file")
37
+ action.add_argument("--fields-file")
38
+ action.set_defaults(handler=_handle_action, format_hint="")
39
+
40
+ log = task_subparsers.add_parser("log", help="读取流程日志")
41
+ log.add_argument("--app-key", required=True)
42
+ log.add_argument("--record-id", required=True, type=int)
43
+ log.add_argument("--workflow-node-id", required=True, type=int)
44
+ log.set_defaults(handler=_handle_log, format_hint="")
45
+
46
+
47
+ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
48
+ return context.task.task_list(
49
+ profile=args.profile,
50
+ task_box=args.task_box,
51
+ flow_status=args.flow_status,
52
+ app_key=args.app_key,
53
+ workflow_node_id=args.workflow_node_id,
54
+ query=args.query,
55
+ page=args.page,
56
+ page_size=args.page_size,
57
+ )
58
+
59
+
60
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
61
+ return context.task.task_get(
62
+ profile=args.profile,
63
+ app_key=args.app_key,
64
+ record_id=args.record_id,
65
+ workflow_node_id=args.workflow_node_id,
66
+ include_candidates=bool(args.include_candidates),
67
+ include_associated_reports=bool(args.include_associated_reports),
68
+ )
69
+
70
+
71
+ def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
72
+ return context.task.task_action_execute(
73
+ profile=args.profile,
74
+ app_key=args.app_key,
75
+ record_id=args.record_id,
76
+ workflow_node_id=args.workflow_node_id,
77
+ action=args.action,
78
+ payload=load_object_arg(args.payload_file, option_name="--payload-file") or {},
79
+ fields=load_object_arg(args.fields_file, option_name="--fields-file") or {},
80
+ )
81
+
82
+
83
+ def _handle_log(args: argparse.Namespace, context: CliContext) -> dict:
84
+ return context.task.task_workflow_log_get(
85
+ profile=args.profile,
86
+ app_key=args.app_key,
87
+ record_id=args.record_id,
88
+ workflow_node_id=args.workflow_node_id,
89
+ )
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from ..context import CliContext
6
+
7
+
8
+ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
9
+ parser = subparsers.add_parser("view", help="视图访问")
10
+ view_subparsers = parser.add_subparsers(dest="view_command", required=True)
11
+
12
+ get_parser = view_subparsers.add_parser("get", help="读取视图资源描述")
13
+ get_parser.add_argument("--view-id", required=True)
14
+ get_parser.set_defaults(handler=_handle_get, format_hint="generic")
15
+
16
+
17
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
18
+ return context.resource.view_get(profile=args.profile, view_id=args.view_id)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+
5
+ from ..context import CliContext
6
+
7
+
8
+ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
9
+ parser = subparsers.add_parser("workspace", help="工作区")
10
+ workspace_subparsers = parser.add_subparsers(dest="workspace_command", required=True)
11
+
12
+ list_parser = workspace_subparsers.add_parser("list", help="列出工作区")
13
+ list_parser.add_argument("--page", type=int, default=1)
14
+ list_parser.add_argument("--page-size", type=int, default=20)
15
+ list_parser.add_argument("--include-external", action="store_true")
16
+ list_parser.set_defaults(handler=_handle_list, format_hint="workspace_list")
17
+
18
+
19
+ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
20
+ return context.workspace.workspace_list(
21
+ profile=args.profile,
22
+ page_num=args.page,
23
+ page_size=args.page_size,
24
+ include_external=bool(args.include_external),
25
+ )
@@ -0,0 +1,60 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from ..backend_client import BackendClient
6
+ from ..session_store import SessionStore
7
+ from ..tools.ai_builder_tools import AiBuilderTools
8
+ from ..tools.app_tools import AppTools
9
+ from ..tools.auth_tools import AuthTools
10
+ from ..tools.code_block_tools import CodeBlockTools
11
+ from ..tools.feedback_tools import FeedbackTools
12
+ from ..tools.file_tools import FileTools
13
+ from ..tools.import_tools import ImportTools
14
+ from ..tools.record_tools import RecordTools
15
+ from ..tools.repository_dev_tools import RepositoryDevTools
16
+ from ..tools.resource_read_tools import ResourceReadTools
17
+ from ..tools.task_context_tools import TaskContextTools
18
+ from ..tools.workspace_tools import WorkspaceTools
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class CliContext:
23
+ sessions: SessionStore
24
+ backend: BackendClient
25
+ auth: AuthTools
26
+ workspace: WorkspaceTools
27
+ app: AppTools
28
+ resource: ResourceReadTools
29
+ record: RecordTools
30
+ code_block: CodeBlockTools
31
+ imports: ImportTools
32
+ task: TaskContextTools
33
+ files: FileTools
34
+ builder_feedback: FeedbackTools
35
+ builder: AiBuilderTools
36
+ repo: RepositoryDevTools
37
+
38
+ def close(self) -> None:
39
+ self.backend.close()
40
+
41
+
42
+ def build_cli_context() -> CliContext:
43
+ sessions = SessionStore()
44
+ backend = BackendClient()
45
+ return CliContext(
46
+ sessions=sessions,
47
+ backend=backend,
48
+ auth=AuthTools(sessions, backend),
49
+ workspace=WorkspaceTools(sessions, backend),
50
+ app=AppTools(sessions, backend),
51
+ resource=ResourceReadTools(sessions, backend),
52
+ record=RecordTools(sessions, backend),
53
+ code_block=CodeBlockTools(sessions, backend),
54
+ imports=ImportTools(sessions, backend),
55
+ task=TaskContextTools(sessions, backend),
56
+ files=FileTools(sessions, backend),
57
+ builder_feedback=FeedbackTools(backend, mcp_side="App Builder MCP"),
58
+ builder=AiBuilderTools(sessions, backend),
59
+ repo=RepositoryDevTools(sessions, backend),
60
+ )
@@ -0,0 +1,334 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, TextIO
5
+
6
+
7
+ def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
8
+ formatter = _FORMATTERS.get(hint, _format_generic)
9
+ text = formatter(result)
10
+ stream.write(text)
11
+ if not text.endswith("\n"):
12
+ stream.write("\n")
13
+
14
+
15
+ def _format_generic(result: dict[str, Any]) -> str:
16
+ lines: list[str] = []
17
+ title = _first_present(result, "status", "message")
18
+ if title:
19
+ lines.append(str(title))
20
+ data = result.get("data")
21
+ if isinstance(data, dict):
22
+ scalar_lines = _dict_scalar_lines(data)
23
+ if scalar_lines:
24
+ lines.extend(scalar_lines)
25
+ elif result:
26
+ scalar_lines = _dict_scalar_lines(result)
27
+ if scalar_lines:
28
+ lines.extend(scalar_lines)
29
+ if not lines:
30
+ lines.append(json.dumps(result, ensure_ascii=False, indent=2))
31
+ _append_warnings(lines, result.get("warnings"))
32
+ _append_verification(lines, result.get("verification"))
33
+ return "\n".join(lines) + "\n"
34
+
35
+
36
+ def _format_whoami(result: dict[str, Any]) -> str:
37
+ lines = [
38
+ f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
39
+ f"UID: {result.get('uid')}",
40
+ f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
41
+ f"QF Version: {result.get('qf_version') or '-'}",
42
+ ]
43
+ lines.append(f"Permission Level: {result.get('permission_level') or '-'}")
44
+ departments = result.get("departments") if isinstance(result.get("departments"), list) else []
45
+ roles = result.get("roles") if isinstance(result.get("roles"), list) else []
46
+ if departments:
47
+ lines.append(
48
+ "Departments: "
49
+ + ", ".join(
50
+ str(item.get("dept_name") or item.get("dept_id"))
51
+ for item in departments
52
+ if isinstance(item, dict)
53
+ )
54
+ )
55
+ if roles:
56
+ lines.append(
57
+ "Roles: "
58
+ + ", ".join(
59
+ str(item.get("role_name") or item.get("role_id"))
60
+ for item in roles
61
+ if isinstance(item, dict)
62
+ )
63
+ )
64
+ _append_warnings(lines, result.get("warnings"))
65
+ return "\n".join(lines) + "\n"
66
+
67
+
68
+ def _format_workspace_list(result: dict[str, Any]) -> str:
69
+ page = result.get("page") if isinstance(result.get("page"), dict) else {}
70
+ items = page.get("list") if isinstance(page.get("list"), list) else []
71
+ rows = []
72
+ for item in items:
73
+ if not isinstance(item, dict):
74
+ continue
75
+ rows.append(
76
+ [
77
+ str(item.get("wsId") or ""),
78
+ str(item.get("workspaceName") or item.get("wsName") or ""),
79
+ str(item.get("remark") or ""),
80
+ ]
81
+ )
82
+ return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
83
+
84
+
85
+ def _format_app_items(result: dict[str, Any]) -> str:
86
+ items = result.get("items")
87
+ if not isinstance(items, list):
88
+ items = result.get("apps")
89
+ rows = []
90
+ for item in items or []:
91
+ if not isinstance(item, dict):
92
+ continue
93
+ rows.append(
94
+ [
95
+ str(item.get("app_key") or item.get("appKey") or ""),
96
+ str(item.get("app_name") or item.get("name") or item.get("title") or ""),
97
+ str(item.get("package_name") or item.get("tag_name") or ""),
98
+ ]
99
+ )
100
+ return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
101
+
102
+
103
+ def _format_app_get(result: dict[str, Any]) -> str:
104
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
105
+ lines = [
106
+ f"App: {data.get('app_name') or '-'}",
107
+ f"App Key: {data.get('app_key') or '-'}",
108
+ f"Can Create: {data.get('can_create')}",
109
+ ]
110
+ import_capability = data.get("import_capability")
111
+ if isinstance(import_capability, dict):
112
+ lines.append(
113
+ "Import Capability: "
114
+ f"{import_capability.get('auth_source') or 'unknown'} / "
115
+ f"can_import={import_capability.get('can_import')}"
116
+ )
117
+ editability = data.get("editability") if isinstance(data.get("editability"), dict) else {}
118
+ if editability:
119
+ lines.append(
120
+ "Editability: "
121
+ f"form={editability.get('can_edit_form')} / "
122
+ f"flow={editability.get('can_edit_flow')} / "
123
+ f"views={editability.get('can_edit_views')} / "
124
+ f"charts={editability.get('can_edit_charts')}"
125
+ )
126
+ views = data.get("accessible_views") if isinstance(data.get("accessible_views"), list) else []
127
+ lines.append(f"Accessible Views: {len(views)}")
128
+ for item in views[:10]:
129
+ if isinstance(item, dict):
130
+ lines.append(f"- {item.get('view_id')}: {item.get('name')}")
131
+ _append_warnings(lines, result.get("warnings"))
132
+ return "\n".join(lines) + "\n"
133
+
134
+ def _format_record_list(result: dict[str, Any]) -> str:
135
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
136
+ items = data.get("items") if isinstance(data.get("items"), list) else []
137
+ lines = [f"Returned Records: {len(items)}"]
138
+ for item in items[:10]:
139
+ if isinstance(item, dict):
140
+ lines.append(json.dumps(item, ensure_ascii=False))
141
+ if len(items) > 10:
142
+ lines.append(f"... {len(items) - 10} more")
143
+ _append_warnings(lines, result.get("warnings"))
144
+ _append_verification(lines, result.get("verification"))
145
+ return "\n".join(lines) + "\n"
146
+
147
+
148
+ def _format_task_list(result: dict[str, Any]) -> str:
149
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
150
+ items = data.get("items") if isinstance(data.get("items"), list) else []
151
+ rows = []
152
+ for item in items:
153
+ if not isinstance(item, dict):
154
+ continue
155
+ rows.append(
156
+ [
157
+ str(item.get("app_key") or ""),
158
+ str(item.get("record_id") or ""),
159
+ str(item.get("workflow_node_id") or ""),
160
+ str(item.get("title") or item.get("task_name") or ""),
161
+ ]
162
+ )
163
+ output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "title"], rows)
164
+ lines = output.rstrip("\n").split("\n")
165
+ _append_warnings(lines, result.get("warnings"))
166
+ return "\n".join(lines) + "\n"
167
+
168
+
169
+ def _format_task_get(result: dict[str, Any]) -> str:
170
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
171
+ task = data.get("task") if isinstance(data.get("task"), dict) else {}
172
+ record = data.get("record") if isinstance(data.get("record"), dict) else {}
173
+ capabilities = data.get("capabilities") if isinstance(data.get("capabilities"), dict) else {}
174
+ update_schema = data.get("update_schema") if isinstance(data.get("update_schema"), dict) else {}
175
+ writable_fields = update_schema.get("writable_fields") if isinstance(update_schema.get("writable_fields"), list) else []
176
+ lines = [
177
+ f"Task: {task.get('app_key') or '-'} / {task.get('record_id') or '-'} / {task.get('workflow_node_id') or '-'}",
178
+ f"Node: {task.get('workflow_node_name') or '-'}",
179
+ f"Apply Status: {record.get('apply_status')}",
180
+ f"Available Actions: {', '.join(str(item) for item in (capabilities.get('available_actions') or [])) or '-'}",
181
+ f"Editable Fields: {len(writable_fields)}",
182
+ ]
183
+ if writable_fields:
184
+ for item in writable_fields[:10]:
185
+ if isinstance(item, dict):
186
+ lines.append(f"- {item.get('title') or '-'} ({item.get('kind') or 'field'})")
187
+ blockers = update_schema.get("blockers") if isinstance(update_schema.get("blockers"), list) else []
188
+ if blockers:
189
+ lines.append("Update Schema Blockers:")
190
+ for item in blockers:
191
+ lines.append(f"- {item}")
192
+ schema_warnings = update_schema.get("warnings") if isinstance(update_schema.get("warnings"), list) else []
193
+ if schema_warnings:
194
+ lines.append("Update Schema Warnings:")
195
+ for item in schema_warnings:
196
+ if isinstance(item, dict):
197
+ lines.append(f"- {item.get('code') or 'WARNING'}: {item.get('message') or ''}".rstrip())
198
+ else:
199
+ lines.append(f"- {item}")
200
+ _append_warnings(lines, result.get("warnings"))
201
+ return "\n".join(lines) + "\n"
202
+
203
+
204
+ def _format_import_verify(result: dict[str, Any]) -> str:
205
+ lines = [
206
+ f"App Key: {result.get('app_key') or '-'}",
207
+ f"File: {result.get('file_name') or result.get('file_path') or '-'}",
208
+ f"Can Import: {result.get('can_import')}",
209
+ f"Apply Rows: {result.get('apply_rows')}",
210
+ f"Verification ID: {result.get('verification_id') or '-'}",
211
+ ]
212
+ issues = result.get("issues") if isinstance(result.get("issues"), list) else []
213
+ if issues:
214
+ lines.append("Issues:")
215
+ for issue in issues:
216
+ if isinstance(issue, dict):
217
+ lines.append(f"- {issue.get('code') or 'ISSUE'}: {issue.get('message') or issue}")
218
+ else:
219
+ lines.append(f"- {issue}")
220
+ _append_warnings(lines, result.get("warnings"))
221
+ _append_verification(lines, result.get("verification"))
222
+ return "\n".join(lines) + "\n"
223
+
224
+
225
+ def _format_import_status(result: dict[str, Any]) -> str:
226
+ lines = [
227
+ f"Status: {result.get('status') or '-'}",
228
+ f"Import ID: {result.get('import_id') or '-'}",
229
+ f"Process ID: {result.get('process_id_str') or '-'}",
230
+ f"Success Rows: {result.get('success_rows') or 0}",
231
+ f"Failed Rows: {result.get('failed_rows') or 0}",
232
+ f"Progress: {result.get('progress') or '-'}",
233
+ ]
234
+ _append_warnings(lines, result.get("warnings"))
235
+ _append_verification(lines, result.get("verification"))
236
+ return "\n".join(lines) + "\n"
237
+
238
+
239
+ def _format_builder_summary(result: dict[str, Any]) -> str:
240
+ lines = []
241
+ if "status" in result:
242
+ lines.append(f"Status: {result.get('status')}")
243
+ if "app_key" in result:
244
+ lines.append(f"App Key: {result.get('app_key')}")
245
+ if "dash_key" in result:
246
+ lines.append(f"Dash Key: {result.get('dash_key')}")
247
+ if "verified" in result:
248
+ lines.append(f"Verified: {result.get('verified')}")
249
+ data = result.get("data")
250
+ if isinstance(data, dict):
251
+ scalar_lines = _dict_scalar_lines(data)
252
+ lines.extend(scalar_lines[:8])
253
+ _append_warnings(lines, result.get("warnings"))
254
+ _append_verification(lines, result.get("verification"))
255
+ if not lines:
256
+ return _format_generic(result)
257
+ return "\n".join(lines) + "\n"
258
+
259
+
260
+ def emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
261
+ json.dump(result, stream, ensure_ascii=False, indent=2)
262
+ stream.write("\n")
263
+
264
+
265
+ def _render_titled_table(title: str, headers: list[str], rows: list[list[str]]) -> str:
266
+ lines = [title]
267
+ if not rows:
268
+ lines.append("(empty)")
269
+ return "\n".join(lines) + "\n"
270
+ widths = [len(header) for header in headers]
271
+ for row in rows:
272
+ for index, cell in enumerate(row):
273
+ widths[index] = max(widths[index], len(cell))
274
+ header_line = " ".join(header.ljust(widths[index]) for index, header in enumerate(headers))
275
+ lines.append(header_line)
276
+ lines.append(" ".join("-" * width for width in widths))
277
+ for row in rows:
278
+ lines.append(" ".join(cell.ljust(widths[index]) for index, cell in enumerate(row)))
279
+ return "\n".join(lines) + "\n"
280
+
281
+
282
+ def _dict_scalar_lines(payload: dict[str, Any]) -> list[str]:
283
+ lines: list[str] = []
284
+ for key, value in payload.items():
285
+ if isinstance(value, (str, int, float, bool)) or value is None:
286
+ lines.append(f"{key}: {value}")
287
+ return lines
288
+
289
+
290
+ def _append_warnings(lines: list[str], warnings: Any) -> None:
291
+ if not isinstance(warnings, list) or not warnings:
292
+ return
293
+ lines.append("Warnings:")
294
+ for warning in warnings:
295
+ if isinstance(warning, dict):
296
+ code = warning.get("code")
297
+ message = warning.get("message")
298
+ if code or message:
299
+ lines.append(f"- {code or 'WARNING'}: {message or ''}".rstrip())
300
+ else:
301
+ lines.append(f"- {json.dumps(warning, ensure_ascii=False)}")
302
+ else:
303
+ lines.append(f"- {warning}")
304
+
305
+
306
+ def _append_verification(lines: list[str], verification: Any) -> None:
307
+ if not isinstance(verification, dict) or not verification:
308
+ return
309
+ lines.append("Verification:")
310
+ for key, value in verification.items():
311
+ if isinstance(value, (str, int, float, bool)) or value is None:
312
+ lines.append(f"- {key}: {value}")
313
+
314
+
315
+ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
316
+ for key in keys:
317
+ if key in payload and payload.get(key) is not None:
318
+ return payload.get(key)
319
+ return None
320
+
321
+
322
+ _FORMATTERS = {
323
+ "auth_whoami": _format_whoami,
324
+ "workspace_list": _format_workspace_list,
325
+ "app_list": _format_app_items,
326
+ "app_search": _format_app_items,
327
+ "app_get": _format_app_get,
328
+ "record_list": _format_record_list,
329
+ "task_list": _format_task_list,
330
+ "task_get": _format_task_get,
331
+ "import_verify": _format_import_verify,
332
+ "import_status": _format_import_status,
333
+ "builder_summary": _format_builder_summary,
334
+ }
@@ -0,0 +1,50 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..errors import QingflowApiError
9
+
10
+
11
+ def load_json_value(path: str, *, option_name: str) -> Any:
12
+ if not path:
13
+ raise QingflowApiError.config_error(f"{option_name} is required")
14
+ if path == "-":
15
+ raw = sys.stdin.read()
16
+ else:
17
+ try:
18
+ raw = Path(path).expanduser().read_text(encoding="utf-8")
19
+ except OSError as exc:
20
+ raise QingflowApiError.config_error(f"failed to read {option_name} from '{path}': {exc}") from exc
21
+ try:
22
+ return json.loads(raw)
23
+ except json.JSONDecodeError as exc:
24
+ raise QingflowApiError.config_error(f"{option_name} must contain valid JSON: {exc}") from exc
25
+
26
+
27
+ def load_json_object(path: str, *, option_name: str) -> dict[str, Any]:
28
+ payload = load_json_value(path, option_name=option_name)
29
+ if not isinstance(payload, dict):
30
+ raise QingflowApiError.config_error(f"{option_name} must be a JSON object")
31
+ return payload
32
+
33
+
34
+ def load_json_list(path: str, *, option_name: str) -> list[Any]:
35
+ payload = load_json_value(path, option_name=option_name)
36
+ if not isinstance(payload, list):
37
+ raise QingflowApiError.config_error(f"{option_name} must be a JSON array")
38
+ return payload
39
+
40
+
41
+ def load_optional_json_object(path: str | None, *, option_name: str) -> dict[str, Any] | None:
42
+ if not path:
43
+ return None
44
+ return load_json_object(path, option_name=option_name)
45
+
46
+
47
+ def load_optional_json_list(path: str | None, *, option_name: str) -> list[Any]:
48
+ if not path:
49
+ return []
50
+ return load_json_list(path, option_name=option_name)