@josephyan/qingflow-cli 0.2.0-beta.55
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 +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2339 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
|
@@ -0,0 +1,87 @@
|
|
|
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="")
|
|
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.set_defaults(handler=_handle_action, format_hint="")
|
|
38
|
+
|
|
39
|
+
log = task_subparsers.add_parser("log", help="读取流程日志")
|
|
40
|
+
log.add_argument("--app-key", required=True)
|
|
41
|
+
log.add_argument("--record-id", required=True, type=int)
|
|
42
|
+
log.add_argument("--workflow-node-id", required=True, type=int)
|
|
43
|
+
log.set_defaults(handler=_handle_log, format_hint="")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
47
|
+
return context.task.task_list(
|
|
48
|
+
profile=args.profile,
|
|
49
|
+
task_box=args.task_box,
|
|
50
|
+
flow_status=args.flow_status,
|
|
51
|
+
app_key=args.app_key,
|
|
52
|
+
workflow_node_id=args.workflow_node_id,
|
|
53
|
+
query=args.query,
|
|
54
|
+
page=args.page,
|
|
55
|
+
page_size=args.page_size,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
60
|
+
return context.task.task_get(
|
|
61
|
+
profile=args.profile,
|
|
62
|
+
app_key=args.app_key,
|
|
63
|
+
record_id=args.record_id,
|
|
64
|
+
workflow_node_id=args.workflow_node_id,
|
|
65
|
+
include_candidates=bool(args.include_candidates),
|
|
66
|
+
include_associated_reports=bool(args.include_associated_reports),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _handle_action(args: argparse.Namespace, context: CliContext) -> dict:
|
|
71
|
+
return context.task.task_action_execute(
|
|
72
|
+
profile=args.profile,
|
|
73
|
+
app_key=args.app_key,
|
|
74
|
+
record_id=args.record_id,
|
|
75
|
+
workflow_node_id=args.workflow_node_id,
|
|
76
|
+
action=args.action,
|
|
77
|
+
payload=load_object_arg(args.payload_file, option_name="--payload-file") or {},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _handle_log(args: argparse.Namespace, context: CliContext) -> dict:
|
|
82
|
+
return context.task.task_workflow_log_get(
|
|
83
|
+
profile=args.profile,
|
|
84
|
+
app_key=args.app_key,
|
|
85
|
+
record_id=args.record_id,
|
|
86
|
+
workflow_node_id=args.workflow_node_id,
|
|
87
|
+
)
|
|
@@ -0,0 +1,33 @@
|
|
|
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
|
+
select = workspace_subparsers.add_parser("select", help="切换工作区")
|
|
19
|
+
select.add_argument("--ws-id", type=int, required=True)
|
|
20
|
+
select.set_defaults(handler=_handle_select, format_hint="")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
24
|
+
return context.workspace.workspace_list(
|
|
25
|
+
profile=args.profile,
|
|
26
|
+
page_num=args.page,
|
|
27
|
+
page_size=args.page_size,
|
|
28
|
+
include_external=bool(args.include_external),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
|
|
33
|
+
return context.workspace.workspace_select(profile=args.profile, ws_id=args.ws_id)
|
|
@@ -0,0 +1,48 @@
|
|
|
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.import_tools import ImportTools
|
|
12
|
+
from ..tools.record_tools import RecordTools
|
|
13
|
+
from ..tools.task_context_tools import TaskContextTools
|
|
14
|
+
from ..tools.workspace_tools import WorkspaceTools
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(slots=True)
|
|
18
|
+
class CliContext:
|
|
19
|
+
sessions: SessionStore
|
|
20
|
+
backend: BackendClient
|
|
21
|
+
auth: AuthTools
|
|
22
|
+
workspace: WorkspaceTools
|
|
23
|
+
app: AppTools
|
|
24
|
+
record: RecordTools
|
|
25
|
+
code_block: CodeBlockTools
|
|
26
|
+
imports: ImportTools
|
|
27
|
+
task: TaskContextTools
|
|
28
|
+
builder: AiBuilderTools
|
|
29
|
+
|
|
30
|
+
def close(self) -> None:
|
|
31
|
+
self.backend.close()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def build_cli_context() -> CliContext:
|
|
35
|
+
sessions = SessionStore()
|
|
36
|
+
backend = BackendClient()
|
|
37
|
+
return CliContext(
|
|
38
|
+
sessions=sessions,
|
|
39
|
+
backend=backend,
|
|
40
|
+
auth=AuthTools(sessions, backend),
|
|
41
|
+
workspace=WorkspaceTools(sessions, backend),
|
|
42
|
+
app=AppTools(sessions, backend),
|
|
43
|
+
record=RecordTools(sessions, backend),
|
|
44
|
+
code_block=CodeBlockTools(sessions, backend),
|
|
45
|
+
imports=ImportTools(sessions, backend),
|
|
46
|
+
task=TaskContextTools(sessions, backend),
|
|
47
|
+
builder=AiBuilderTools(sessions, backend),
|
|
48
|
+
)
|
|
@@ -0,0 +1,269 @@
|
|
|
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"Profile: {result.get('profile')}",
|
|
39
|
+
f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
|
|
40
|
+
f"UID: {result.get('uid')}",
|
|
41
|
+
f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
|
|
42
|
+
f"Base URL: {result.get('base_url')}",
|
|
43
|
+
]
|
|
44
|
+
return "\n".join(lines) + "\n"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _format_workspace_list(result: dict[str, Any]) -> str:
|
|
48
|
+
page = result.get("page") if isinstance(result.get("page"), dict) else {}
|
|
49
|
+
items = page.get("list") if isinstance(page.get("list"), list) else []
|
|
50
|
+
rows = []
|
|
51
|
+
for item in items:
|
|
52
|
+
if not isinstance(item, dict):
|
|
53
|
+
continue
|
|
54
|
+
rows.append(
|
|
55
|
+
[
|
|
56
|
+
str(item.get("wsId") or ""),
|
|
57
|
+
str(item.get("workspaceName") or item.get("wsName") or ""),
|
|
58
|
+
str(item.get("remark") or ""),
|
|
59
|
+
]
|
|
60
|
+
)
|
|
61
|
+
return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _format_app_items(result: dict[str, Any]) -> str:
|
|
65
|
+
items = result.get("items")
|
|
66
|
+
if not isinstance(items, list):
|
|
67
|
+
items = result.get("apps")
|
|
68
|
+
rows = []
|
|
69
|
+
for item in items or []:
|
|
70
|
+
if not isinstance(item, dict):
|
|
71
|
+
continue
|
|
72
|
+
rows.append(
|
|
73
|
+
[
|
|
74
|
+
str(item.get("app_key") or item.get("appKey") or ""),
|
|
75
|
+
str(item.get("app_name") or item.get("name") or item.get("title") or ""),
|
|
76
|
+
str(item.get("package_name") or item.get("tag_name") or ""),
|
|
77
|
+
]
|
|
78
|
+
)
|
|
79
|
+
return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _format_app_get(result: dict[str, Any]) -> str:
|
|
83
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
84
|
+
lines = [
|
|
85
|
+
f"App: {data.get('app_name') or '-'}",
|
|
86
|
+
f"App Key: {data.get('app_key') or '-'}",
|
|
87
|
+
f"Can Create: {data.get('can_create')}",
|
|
88
|
+
]
|
|
89
|
+
import_capability = data.get("import_capability")
|
|
90
|
+
if isinstance(import_capability, dict):
|
|
91
|
+
lines.append(
|
|
92
|
+
"Import Capability: "
|
|
93
|
+
f"{import_capability.get('auth_source') or 'unknown'} / "
|
|
94
|
+
f"can_import={import_capability.get('can_import')}"
|
|
95
|
+
)
|
|
96
|
+
views = data.get("accessible_views") if isinstance(data.get("accessible_views"), list) else []
|
|
97
|
+
lines.append(f"Accessible Views: {len(views)}")
|
|
98
|
+
for item in views[:10]:
|
|
99
|
+
if isinstance(item, dict):
|
|
100
|
+
lines.append(f"- {item.get('view_id')}: {item.get('name')}")
|
|
101
|
+
_append_warnings(lines, result.get("warnings"))
|
|
102
|
+
return "\n".join(lines) + "\n"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _format_record_list(result: dict[str, Any]) -> str:
|
|
106
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
107
|
+
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
108
|
+
lines = [f"Returned Records: {len(items)}"]
|
|
109
|
+
for item in items[:10]:
|
|
110
|
+
if isinstance(item, dict):
|
|
111
|
+
lines.append(json.dumps(item, ensure_ascii=False))
|
|
112
|
+
if len(items) > 10:
|
|
113
|
+
lines.append(f"... {len(items) - 10} more")
|
|
114
|
+
_append_warnings(lines, result.get("warnings"))
|
|
115
|
+
_append_verification(lines, result.get("verification"))
|
|
116
|
+
return "\n".join(lines) + "\n"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _format_task_list(result: dict[str, Any]) -> str:
|
|
120
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
121
|
+
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
122
|
+
rows = []
|
|
123
|
+
for item in items:
|
|
124
|
+
if not isinstance(item, dict):
|
|
125
|
+
continue
|
|
126
|
+
rows.append(
|
|
127
|
+
[
|
|
128
|
+
str(item.get("app_key") or ""),
|
|
129
|
+
str(item.get("record_id") or ""),
|
|
130
|
+
str(item.get("workflow_node_id") or ""),
|
|
131
|
+
str(item.get("title") or item.get("task_name") or ""),
|
|
132
|
+
]
|
|
133
|
+
)
|
|
134
|
+
output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "title"], rows)
|
|
135
|
+
lines = output.rstrip("\n").split("\n")
|
|
136
|
+
_append_warnings(lines, result.get("warnings"))
|
|
137
|
+
return "\n".join(lines) + "\n"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _format_import_verify(result: dict[str, Any]) -> str:
|
|
141
|
+
lines = [
|
|
142
|
+
f"App Key: {result.get('app_key') or '-'}",
|
|
143
|
+
f"File: {result.get('file_name') or result.get('file_path') or '-'}",
|
|
144
|
+
f"Can Import: {result.get('can_import')}",
|
|
145
|
+
f"Apply Rows: {result.get('apply_rows')}",
|
|
146
|
+
f"Verification ID: {result.get('verification_id') or '-'}",
|
|
147
|
+
]
|
|
148
|
+
issues = result.get("issues") if isinstance(result.get("issues"), list) else []
|
|
149
|
+
if issues:
|
|
150
|
+
lines.append("Issues:")
|
|
151
|
+
for issue in issues:
|
|
152
|
+
if isinstance(issue, dict):
|
|
153
|
+
lines.append(f"- {issue.get('code') or 'ISSUE'}: {issue.get('message') or issue}")
|
|
154
|
+
else:
|
|
155
|
+
lines.append(f"- {issue}")
|
|
156
|
+
_append_warnings(lines, result.get("warnings"))
|
|
157
|
+
_append_verification(lines, result.get("verification"))
|
|
158
|
+
return "\n".join(lines) + "\n"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _format_import_status(result: dict[str, Any]) -> str:
|
|
162
|
+
lines = [
|
|
163
|
+
f"Status: {result.get('status') or '-'}",
|
|
164
|
+
f"Import ID: {result.get('import_id') or '-'}",
|
|
165
|
+
f"Process ID: {result.get('process_id_str') or '-'}",
|
|
166
|
+
f"Success Rows: {result.get('success_rows') or 0}",
|
|
167
|
+
f"Failed Rows: {result.get('failed_rows') or 0}",
|
|
168
|
+
f"Progress: {result.get('progress') or '-'}",
|
|
169
|
+
]
|
|
170
|
+
_append_warnings(lines, result.get("warnings"))
|
|
171
|
+
_append_verification(lines, result.get("verification"))
|
|
172
|
+
return "\n".join(lines) + "\n"
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
176
|
+
lines = []
|
|
177
|
+
if "status" in result:
|
|
178
|
+
lines.append(f"Status: {result.get('status')}")
|
|
179
|
+
if "app_key" in result:
|
|
180
|
+
lines.append(f"App Key: {result.get('app_key')}")
|
|
181
|
+
if "dash_key" in result:
|
|
182
|
+
lines.append(f"Dash Key: {result.get('dash_key')}")
|
|
183
|
+
if "verified" in result:
|
|
184
|
+
lines.append(f"Verified: {result.get('verified')}")
|
|
185
|
+
data = result.get("data")
|
|
186
|
+
if isinstance(data, dict):
|
|
187
|
+
scalar_lines = _dict_scalar_lines(data)
|
|
188
|
+
lines.extend(scalar_lines[:8])
|
|
189
|
+
_append_warnings(lines, result.get("warnings"))
|
|
190
|
+
_append_verification(lines, result.get("verification"))
|
|
191
|
+
if not lines:
|
|
192
|
+
return _format_generic(result)
|
|
193
|
+
return "\n".join(lines) + "\n"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
|
|
197
|
+
json.dump(result, stream, ensure_ascii=False, indent=2)
|
|
198
|
+
stream.write("\n")
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _render_titled_table(title: str, headers: list[str], rows: list[list[str]]) -> str:
|
|
202
|
+
lines = [title]
|
|
203
|
+
if not rows:
|
|
204
|
+
lines.append("(empty)")
|
|
205
|
+
return "\n".join(lines) + "\n"
|
|
206
|
+
widths = [len(header) for header in headers]
|
|
207
|
+
for row in rows:
|
|
208
|
+
for index, cell in enumerate(row):
|
|
209
|
+
widths[index] = max(widths[index], len(cell))
|
|
210
|
+
header_line = " ".join(header.ljust(widths[index]) for index, header in enumerate(headers))
|
|
211
|
+
lines.append(header_line)
|
|
212
|
+
lines.append(" ".join("-" * width for width in widths))
|
|
213
|
+
for row in rows:
|
|
214
|
+
lines.append(" ".join(cell.ljust(widths[index]) for index, cell in enumerate(row)))
|
|
215
|
+
return "\n".join(lines) + "\n"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _dict_scalar_lines(payload: dict[str, Any]) -> list[str]:
|
|
219
|
+
lines: list[str] = []
|
|
220
|
+
for key, value in payload.items():
|
|
221
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
222
|
+
lines.append(f"{key}: {value}")
|
|
223
|
+
return lines
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _append_warnings(lines: list[str], warnings: Any) -> None:
|
|
227
|
+
if not isinstance(warnings, list) or not warnings:
|
|
228
|
+
return
|
|
229
|
+
lines.append("Warnings:")
|
|
230
|
+
for warning in warnings:
|
|
231
|
+
if isinstance(warning, dict):
|
|
232
|
+
code = warning.get("code")
|
|
233
|
+
message = warning.get("message")
|
|
234
|
+
if code or message:
|
|
235
|
+
lines.append(f"- {code or 'WARNING'}: {message or ''}".rstrip())
|
|
236
|
+
else:
|
|
237
|
+
lines.append(f"- {json.dumps(warning, ensure_ascii=False)}")
|
|
238
|
+
else:
|
|
239
|
+
lines.append(f"- {warning}")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _append_verification(lines: list[str], verification: Any) -> None:
|
|
243
|
+
if not isinstance(verification, dict) or not verification:
|
|
244
|
+
return
|
|
245
|
+
lines.append("Verification:")
|
|
246
|
+
for key, value in verification.items():
|
|
247
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
248
|
+
lines.append(f"- {key}: {value}")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _first_present(payload: dict[str, Any], *keys: str) -> Any:
|
|
252
|
+
for key in keys:
|
|
253
|
+
if key in payload and payload.get(key) is not None:
|
|
254
|
+
return payload.get(key)
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
_FORMATTERS = {
|
|
259
|
+
"auth_whoami": _format_whoami,
|
|
260
|
+
"workspace_list": _format_workspace_list,
|
|
261
|
+
"app_list": _format_app_items,
|
|
262
|
+
"app_search": _format_app_items,
|
|
263
|
+
"app_get": _format_app_get,
|
|
264
|
+
"record_list": _format_record_list,
|
|
265
|
+
"task_list": _format_task_list,
|
|
266
|
+
"import_verify": _format_import_verify,
|
|
267
|
+
"import_status": _format_import_status,
|
|
268
|
+
"builder_summary": _format_builder_summary,
|
|
269
|
+
}
|
|
@@ -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)
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Callable, TextIO
|
|
7
|
+
|
|
8
|
+
from ..errors import QingflowApiError
|
|
9
|
+
from .context import CliContext, build_cli_context
|
|
10
|
+
from .formatters import emit_json_result, emit_text_result
|
|
11
|
+
from .commands import register_all_commands
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
18
|
+
parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI")
|
|
19
|
+
parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
|
|
20
|
+
parser.add_argument("--json", action="store_true", help="输出原始 JSON")
|
|
21
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
|
+
register_all_commands(subparsers)
|
|
23
|
+
return parser
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main(argv: list[str] | None = None) -> None:
|
|
27
|
+
raise SystemExit(run(argv))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run(
|
|
31
|
+
argv: list[str] | None = None,
|
|
32
|
+
*,
|
|
33
|
+
context_factory: Callable[[], CliContext] = build_cli_context,
|
|
34
|
+
stdout: TextIO | None = None,
|
|
35
|
+
stderr: TextIO | None = None,
|
|
36
|
+
) -> int:
|
|
37
|
+
out = stdout or sys.stdout
|
|
38
|
+
err = stderr or sys.stderr
|
|
39
|
+
parser = build_parser()
|
|
40
|
+
normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
|
|
41
|
+
try:
|
|
42
|
+
args = parser.parse_args(normalized_argv)
|
|
43
|
+
except SystemExit as exc:
|
|
44
|
+
return int(exc.code or 0)
|
|
45
|
+
handler = getattr(args, "handler", None)
|
|
46
|
+
if handler is None:
|
|
47
|
+
parser.print_help(out)
|
|
48
|
+
return 2
|
|
49
|
+
context = context_factory()
|
|
50
|
+
try:
|
|
51
|
+
result = handler(args, context)
|
|
52
|
+
except RuntimeError as exc:
|
|
53
|
+
payload = _parse_error_payload(exc)
|
|
54
|
+
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
55
|
+
except QingflowApiError as exc:
|
|
56
|
+
payload = exc.to_dict()
|
|
57
|
+
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
58
|
+
finally:
|
|
59
|
+
context.close()
|
|
60
|
+
|
|
61
|
+
exit_code = _result_exit_code(result)
|
|
62
|
+
stream = out if bool(args.json) or exit_code == 0 else err
|
|
63
|
+
if bool(args.json):
|
|
64
|
+
emit_json_result(result, stream=stream)
|
|
65
|
+
else:
|
|
66
|
+
emit_text_result(result, hint=getattr(args, "format_hint", ""), stream=stream)
|
|
67
|
+
return exit_code
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
71
|
+
global_args: list[str] = []
|
|
72
|
+
remaining: list[str] = []
|
|
73
|
+
index = 0
|
|
74
|
+
while index < len(argv):
|
|
75
|
+
token = argv[index]
|
|
76
|
+
if token == "--json":
|
|
77
|
+
global_args.append(token)
|
|
78
|
+
index += 1
|
|
79
|
+
continue
|
|
80
|
+
if token == "--profile":
|
|
81
|
+
global_args.append(token)
|
|
82
|
+
if index + 1 >= len(argv):
|
|
83
|
+
global_args.append("")
|
|
84
|
+
break
|
|
85
|
+
global_args.append(argv[index + 1])
|
|
86
|
+
index += 2
|
|
87
|
+
continue
|
|
88
|
+
if token.startswith("--profile="):
|
|
89
|
+
global_args.append(token)
|
|
90
|
+
index += 1
|
|
91
|
+
continue
|
|
92
|
+
remaining.append(token)
|
|
93
|
+
index += 1
|
|
94
|
+
return global_args + remaining
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
|
|
98
|
+
raw = str(exc)
|
|
99
|
+
try:
|
|
100
|
+
payload = json.loads(raw)
|
|
101
|
+
except json.JSONDecodeError:
|
|
102
|
+
return {"category": "runtime", "message": raw}
|
|
103
|
+
return payload if isinstance(payload, dict) else {"category": "runtime", "message": raw}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, stderr: TextIO) -> int:
|
|
107
|
+
exit_code = _error_exit_code(payload)
|
|
108
|
+
if json_mode:
|
|
109
|
+
emit_json_result(payload, stream=stdout)
|
|
110
|
+
return exit_code
|
|
111
|
+
lines = [
|
|
112
|
+
f"Category: {payload.get('category') or 'error'}",
|
|
113
|
+
f"Message: {payload.get('message') or 'Unknown error'}",
|
|
114
|
+
]
|
|
115
|
+
if payload.get("backend_code") is not None:
|
|
116
|
+
lines.append(f"Backend Code: {payload.get('backend_code')}")
|
|
117
|
+
if payload.get("request_id"):
|
|
118
|
+
lines.append(f"Request ID: {payload.get('request_id')}")
|
|
119
|
+
details = payload.get("details")
|
|
120
|
+
if isinstance(details, dict):
|
|
121
|
+
for key, value in details.items():
|
|
122
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
123
|
+
lines.append(f"{key}: {value}")
|
|
124
|
+
stderr.write("\n".join(lines) + "\n")
|
|
125
|
+
return exit_code
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _error_exit_code(payload: dict[str, Any]) -> int:
|
|
129
|
+
category = str(payload.get("category") or "").lower()
|
|
130
|
+
if category in {"auth", "workspace"}:
|
|
131
|
+
return 3
|
|
132
|
+
return 4
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _result_exit_code(result: dict[str, Any]) -> int:
|
|
136
|
+
if not isinstance(result, dict):
|
|
137
|
+
return 0
|
|
138
|
+
if result.get("ok") is False:
|
|
139
|
+
return 4
|
|
140
|
+
status = str(result.get("status") or "").lower()
|
|
141
|
+
if status in {"failed", "blocked"}:
|
|
142
|
+
return 4
|
|
143
|
+
return 0
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
if __name__ == "__main__":
|
|
147
|
+
main()
|