@josephyan/qingflow-cli 0.2.0-beta.55 → 0.2.0-beta.56
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/cli/commands/app.py +16 -16
- package/src/qingflow_mcp/cli/commands/auth.py +16 -19
- package/src/qingflow_mcp/cli/commands/builder.py +139 -124
- package/src/qingflow_mcp/cli/commands/common.py +95 -21
- package/src/qingflow_mcp/cli/commands/imports.py +34 -42
- package/src/qingflow_mcp/cli/commands/record.py +133 -131
- package/src/qingflow_mcp/cli/commands/task.py +44 -43
- package/src/qingflow_mcp/cli/commands/workspace.py +10 -10
- package/src/qingflow_mcp/cli/context.py +32 -35
- package/src/qingflow_mcp/cli/formatters.py +121 -124
- package/src/qingflow_mcp/cli/main.py +17 -52
- package/src/qingflow_mcp/ops/__init__.py +3 -0
- package/src/qingflow_mcp/ops/apps.py +64 -0
- package/src/qingflow_mcp/ops/auth.py +121 -0
- package/src/qingflow_mcp/ops/base.py +290 -0
- package/src/qingflow_mcp/ops/builder.py +323 -0
- package/src/qingflow_mcp/ops/context.py +120 -0
- package/src/qingflow_mcp/ops/directory.py +171 -0
- package/src/qingflow_mcp/ops/feedback.py +49 -0
- package/src/qingflow_mcp/ops/files.py +78 -0
- package/src/qingflow_mcp/ops/imports.py +140 -0
- package/src/qingflow_mcp/ops/records.py +415 -0
- package/src/qingflow_mcp/ops/tasks.py +171 -0
- package/src/qingflow_mcp/ops/workspace.py +76 -0
- package/src/qingflow_mcp/server_app_builder.py +190 -122
- package/src/qingflow_mcp/server_app_user.py +662 -63
|
@@ -2,47 +2,44 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
|
|
5
|
-
from ..
|
|
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
|
|
5
|
+
from ..ops.context import OperationsRuntime, build_operations_runtime
|
|
15
6
|
|
|
16
7
|
|
|
17
8
|
@dataclass(slots=True)
|
|
18
9
|
class CliContext:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
10
|
+
runtime: OperationsRuntime
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def auth(self):
|
|
14
|
+
return self.runtime.auth
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def workspace(self):
|
|
18
|
+
return self.runtime.workspace
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def apps(self):
|
|
22
|
+
return self.runtime.apps
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def records(self):
|
|
26
|
+
return self.runtime.records
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def imports(self):
|
|
30
|
+
return self.runtime.imports
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def tasks(self):
|
|
34
|
+
return self.runtime.tasks
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def build(self):
|
|
38
|
+
return self.runtime.builder
|
|
29
39
|
|
|
30
40
|
def close(self) -> None:
|
|
31
|
-
self.
|
|
41
|
+
self.runtime.close()
|
|
32
42
|
|
|
33
43
|
|
|
34
44
|
def build_cli_context() -> CliContext:
|
|
35
|
-
|
|
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
|
-
)
|
|
45
|
+
return CliContext(runtime=build_operations_runtime())
|
|
@@ -3,8 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Any, TextIO
|
|
5
5
|
|
|
6
|
+
from ..ops.base import public_result
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
|
|
10
|
+
result = public_result(result)
|
|
8
11
|
formatter = _FORMATTERS.get(hint, _format_generic)
|
|
9
12
|
text = formatter(result)
|
|
10
13
|
stream.write(text)
|
|
@@ -12,61 +15,65 @@ def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> No
|
|
|
12
15
|
stream.write("\n")
|
|
13
16
|
|
|
14
17
|
|
|
18
|
+
def emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
|
|
19
|
+
json.dump(public_result(result), stream, ensure_ascii=False, indent=2)
|
|
20
|
+
stream.write("\n")
|
|
21
|
+
|
|
22
|
+
|
|
15
23
|
def _format_generic(result: dict[str, Any]) -> str:
|
|
16
|
-
lines
|
|
17
|
-
title = _first_present(result, "status", "message")
|
|
18
|
-
if title:
|
|
19
|
-
lines.append(str(title))
|
|
24
|
+
lines = [f"{result.get('message') or result.get('code') or '完成'}"]
|
|
20
25
|
data = result.get("data")
|
|
21
26
|
if isinstance(data, dict):
|
|
22
|
-
|
|
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))
|
|
27
|
+
lines.extend(_scalar_lines(data, limit=10))
|
|
31
28
|
_append_warnings(lines, result.get("warnings"))
|
|
32
|
-
_append_verification(lines, result.get("verification"))
|
|
33
29
|
return "\n".join(lines) + "\n"
|
|
34
30
|
|
|
35
31
|
|
|
36
|
-
def
|
|
32
|
+
def _format_me(result: dict[str, Any]) -> str:
|
|
33
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
34
|
+
user = data.get("user") if isinstance(data.get("user"), dict) else {}
|
|
35
|
+
workspace = data.get("workspace") if isinstance(data.get("workspace"), dict) else {}
|
|
37
36
|
lines = [
|
|
38
|
-
f"Profile: {result.get('profile')}",
|
|
39
|
-
f"User: {
|
|
40
|
-
f"UID: {
|
|
41
|
-
f"Workspace: {
|
|
42
|
-
f"Base URL: {
|
|
37
|
+
f"Profile: {data.get('profile') or result.get('meta', {}).get('profile') or '-'}",
|
|
38
|
+
f"User: {user.get('nick_name') or '-'} ({user.get('email') or '-'})",
|
|
39
|
+
f"UID: {user.get('uid') or '-'}",
|
|
40
|
+
f"Workspace: {workspace.get('name') or '-'} ({workspace.get('ws_id') or '-'})",
|
|
41
|
+
f"Base URL: {data.get('base_url') or '-'}",
|
|
43
42
|
]
|
|
44
43
|
return "\n".join(lines) + "\n"
|
|
45
44
|
|
|
46
45
|
|
|
47
46
|
def _format_workspace_list(result: dict[str, Any]) -> str:
|
|
48
|
-
|
|
49
|
-
items =
|
|
50
|
-
rows = [
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
]
|
|
60
|
-
)
|
|
47
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
48
|
+
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
49
|
+
rows = [
|
|
50
|
+
[
|
|
51
|
+
str(item.get("ws_id") or ""),
|
|
52
|
+
str(item.get("name") or ""),
|
|
53
|
+
str(item.get("remark") or ""),
|
|
54
|
+
]
|
|
55
|
+
for item in items
|
|
56
|
+
if isinstance(item, dict)
|
|
57
|
+
]
|
|
61
58
|
return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
|
|
62
59
|
|
|
63
60
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
def _format_workspace_use(result: dict[str, Any]) -> str:
|
|
62
|
+
workspace = result.get("data", {}).get("workspace") if isinstance(result.get("data"), dict) else {}
|
|
63
|
+
lines = [
|
|
64
|
+
f"Workspace: {workspace.get('name') or '-'}",
|
|
65
|
+
f"WS ID: {workspace.get('ws_id') or '-'}",
|
|
66
|
+
f"QF Version: {result.get('data', {}).get('qf_version') or '-'}",
|
|
67
|
+
f"QF Version Source: {result.get('data', {}).get('qf_version_source') or '-'}",
|
|
68
|
+
]
|
|
69
|
+
return "\n".join(lines) + "\n"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _format_apps_list(result: dict[str, Any]) -> str:
|
|
73
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
74
|
+
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
68
75
|
rows = []
|
|
69
|
-
for item in items
|
|
76
|
+
for item in items:
|
|
70
77
|
if not isinstance(item, dict):
|
|
71
78
|
continue
|
|
72
79
|
rows.append(
|
|
@@ -79,7 +86,7 @@ def _format_app_items(result: dict[str, Any]) -> str:
|
|
|
79
86
|
return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
|
|
80
87
|
|
|
81
88
|
|
|
82
|
-
def
|
|
89
|
+
def _format_app_show(result: dict[str, Any]) -> str:
|
|
83
90
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
84
91
|
lines = [
|
|
85
92
|
f"App: {data.get('app_name') or '-'}",
|
|
@@ -90,8 +97,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
90
97
|
if isinstance(import_capability, dict):
|
|
91
98
|
lines.append(
|
|
92
99
|
"Import Capability: "
|
|
93
|
-
f"{import_capability.get('auth_source') or 'unknown'} / "
|
|
94
|
-
f"can_import={import_capability.get('can_import')}"
|
|
100
|
+
f"{import_capability.get('auth_source') or 'unknown'} / can_import={import_capability.get('can_import')}"
|
|
95
101
|
)
|
|
96
102
|
views = data.get("accessible_views") if isinstance(data.get("accessible_views"), list) else []
|
|
97
103
|
lines.append(f"Accessible Views: {len(views)}")
|
|
@@ -102,7 +108,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
102
108
|
return "\n".join(lines) + "\n"
|
|
103
109
|
|
|
104
110
|
|
|
105
|
-
def
|
|
111
|
+
def _format_records_list(result: dict[str, Any]) -> str:
|
|
106
112
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
107
113
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
108
114
|
lines = [f"Returned Records: {len(items)}"]
|
|
@@ -112,40 +118,43 @@ def _format_record_list(result: dict[str, Any]) -> str:
|
|
|
112
118
|
if len(items) > 10:
|
|
113
119
|
lines.append(f"... {len(items) - 10} more")
|
|
114
120
|
_append_warnings(lines, result.get("warnings"))
|
|
115
|
-
_append_verification(lines, result.get("verification"))
|
|
116
121
|
return "\n".join(lines) + "\n"
|
|
117
122
|
|
|
118
123
|
|
|
119
|
-
def
|
|
124
|
+
def _format_record_show(result: dict[str, Any]) -> str:
|
|
120
125
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
|
|
126
|
+
record = data.get("record")
|
|
127
|
+
if isinstance(record, dict):
|
|
128
|
+
lines = [f"Record ID: {data.get('record_id') or '-'}"]
|
|
129
|
+
lines.extend(_scalar_lines(record, limit=12))
|
|
130
|
+
_append_warnings(lines, result.get("warnings"))
|
|
131
|
+
return "\n".join(lines) + "\n"
|
|
132
|
+
return _format_generic(result)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _format_record_write(result: dict[str, Any]) -> str:
|
|
136
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
137
|
+
lines = [
|
|
138
|
+
f"Code: {result.get('code')}",
|
|
139
|
+
f"Message: {result.get('message')}",
|
|
140
|
+
]
|
|
141
|
+
if data.get("record_id") is not None:
|
|
142
|
+
lines.append(f"Record ID: {data.get('record_id')}")
|
|
143
|
+
if isinstance(data.get("record_ids"), list):
|
|
144
|
+
lines.append(f"Record IDs: {', '.join(str(item) for item in data['record_ids'])}")
|
|
136
145
|
_append_warnings(lines, result.get("warnings"))
|
|
137
146
|
return "\n".join(lines) + "\n"
|
|
138
147
|
|
|
139
148
|
|
|
140
149
|
def _format_import_verify(result: dict[str, Any]) -> str:
|
|
150
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
141
151
|
lines = [
|
|
142
|
-
f"
|
|
143
|
-
f"
|
|
144
|
-
f"
|
|
145
|
-
f"
|
|
146
|
-
f"Verification ID: {result.get('verification_id') or '-'}",
|
|
152
|
+
f"File: {data.get('file_name') or data.get('file_path') or '-'}",
|
|
153
|
+
f"Can Import: {data.get('can_import')}",
|
|
154
|
+
f"Apply Rows: {data.get('apply_rows')}",
|
|
155
|
+
f"Verification ID: {data.get('verification_id') or '-'}",
|
|
147
156
|
]
|
|
148
|
-
issues =
|
|
157
|
+
issues = data.get("issues") if isinstance(data.get("issues"), list) else []
|
|
149
158
|
if issues:
|
|
150
159
|
lines.append("Issues:")
|
|
151
160
|
for issue in issues:
|
|
@@ -154,48 +163,51 @@ def _format_import_verify(result: dict[str, Any]) -> str:
|
|
|
154
163
|
else:
|
|
155
164
|
lines.append(f"- {issue}")
|
|
156
165
|
_append_warnings(lines, result.get("warnings"))
|
|
157
|
-
_append_verification(lines, result.get("verification"))
|
|
158
166
|
return "\n".join(lines) + "\n"
|
|
159
167
|
|
|
160
168
|
|
|
161
169
|
def _format_import_status(result: dict[str, Any]) -> str:
|
|
170
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
162
171
|
lines = [
|
|
163
|
-
f"Status: {
|
|
164
|
-
f"Import ID: {
|
|
165
|
-
f"Process ID: {
|
|
166
|
-
f"Success Rows: {
|
|
167
|
-
f"Failed Rows: {
|
|
168
|
-
f"Progress: {
|
|
172
|
+
f"Status: {data.get('status') or '-'}",
|
|
173
|
+
f"Import ID: {data.get('import_id') or '-'}",
|
|
174
|
+
f"Process ID: {data.get('process_id_str') or '-'}",
|
|
175
|
+
f"Success Rows: {data.get('success_rows') or 0}",
|
|
176
|
+
f"Failed Rows: {data.get('failed_rows') or 0}",
|
|
177
|
+
f"Progress: {data.get('progress') or '-'}",
|
|
169
178
|
]
|
|
170
179
|
_append_warnings(lines, result.get("warnings"))
|
|
171
|
-
_append_verification(lines, result.get("verification"))
|
|
172
180
|
return "\n".join(lines) + "\n"
|
|
173
181
|
|
|
174
182
|
|
|
175
|
-
def
|
|
176
|
-
|
|
177
|
-
if "
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
183
|
+
def _format_tasks_list(result: dict[str, Any]) -> str:
|
|
184
|
+
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
185
|
+
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
186
|
+
rows = []
|
|
187
|
+
for item in items:
|
|
188
|
+
if not isinstance(item, dict):
|
|
189
|
+
continue
|
|
190
|
+
rows.append(
|
|
191
|
+
[
|
|
192
|
+
str(item.get("app_key") or ""),
|
|
193
|
+
str(item.get("record_id") or ""),
|
|
194
|
+
str(item.get("workflow_node_id") or ""),
|
|
195
|
+
str(item.get("title") or ""),
|
|
196
|
+
]
|
|
197
|
+
)
|
|
198
|
+
output = _render_titled_table("Tasks", ["app_key", "record_id", "node_id", "title"], rows)
|
|
199
|
+
lines = output.rstrip("\n").split("\n")
|
|
189
200
|
_append_warnings(lines, result.get("warnings"))
|
|
190
|
-
_append_verification(lines, result.get("verification"))
|
|
191
|
-
if not lines:
|
|
192
|
-
return _format_generic(result)
|
|
193
201
|
return "\n".join(lines) + "\n"
|
|
194
202
|
|
|
195
203
|
|
|
196
|
-
def
|
|
197
|
-
|
|
198
|
-
|
|
204
|
+
def _format_builder_result(result: dict[str, Any]) -> str:
|
|
205
|
+
lines = [f"Code: {result.get('code')}", f"Message: {result.get('message')}"]
|
|
206
|
+
data = result.get("data")
|
|
207
|
+
if isinstance(data, dict):
|
|
208
|
+
lines.extend(_scalar_lines(data, limit=10))
|
|
209
|
+
_append_warnings(lines, result.get("warnings"))
|
|
210
|
+
return "\n".join(lines) + "\n"
|
|
199
211
|
|
|
200
212
|
|
|
201
213
|
def _render_titled_table(title: str, headers: list[str], rows: list[list[str]]) -> str:
|
|
@@ -207,19 +219,20 @@ def _render_titled_table(title: str, headers: list[str], rows: list[list[str]])
|
|
|
207
219
|
for row in rows:
|
|
208
220
|
for index, cell in enumerate(row):
|
|
209
221
|
widths[index] = max(widths[index], len(cell))
|
|
210
|
-
|
|
211
|
-
lines.append(header_line)
|
|
222
|
+
lines.append(" ".join(header.ljust(widths[index]) for index, header in enumerate(headers)))
|
|
212
223
|
lines.append(" ".join("-" * width for width in widths))
|
|
213
224
|
for row in rows:
|
|
214
225
|
lines.append(" ".join(cell.ljust(widths[index]) for index, cell in enumerate(row)))
|
|
215
226
|
return "\n".join(lines) + "\n"
|
|
216
227
|
|
|
217
228
|
|
|
218
|
-
def
|
|
229
|
+
def _scalar_lines(payload: dict[str, Any], *, limit: int) -> list[str]:
|
|
219
230
|
lines: list[str] = []
|
|
220
231
|
for key, value in payload.items():
|
|
221
232
|
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
222
233
|
lines.append(f"{key}: {value}")
|
|
234
|
+
if len(lines) >= limit:
|
|
235
|
+
break
|
|
223
236
|
return lines
|
|
224
237
|
|
|
225
238
|
|
|
@@ -231,39 +244,23 @@ def _append_warnings(lines: list[str], warnings: Any) -> None:
|
|
|
231
244
|
if isinstance(warning, dict):
|
|
232
245
|
code = warning.get("code")
|
|
233
246
|
message = warning.get("message")
|
|
234
|
-
|
|
235
|
-
lines.append(f"- {code or 'WARNING'}: {message or ''}".rstrip())
|
|
236
|
-
else:
|
|
237
|
-
lines.append(f"- {json.dumps(warning, ensure_ascii=False)}")
|
|
247
|
+
lines.append(f"- {code or 'WARNING'}: {message or ''}".rstrip())
|
|
238
248
|
else:
|
|
239
249
|
lines.append(f"- {warning}")
|
|
240
250
|
|
|
241
251
|
|
|
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
252
|
_FORMATTERS = {
|
|
259
|
-
"
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
265
|
-
"
|
|
253
|
+
"me": _format_me,
|
|
254
|
+
"session": _format_me,
|
|
255
|
+
"ws_list": _format_workspace_list,
|
|
256
|
+
"ws_use": _format_workspace_use,
|
|
257
|
+
"apps_list": _format_apps_list,
|
|
258
|
+
"app_show": _format_app_show,
|
|
259
|
+
"records_list": _format_records_list,
|
|
260
|
+
"record_show": _format_record_show,
|
|
261
|
+
"record_write": _format_record_write,
|
|
266
262
|
"import_verify": _format_import_verify,
|
|
267
263
|
"import_status": _format_import_status,
|
|
268
|
-
"
|
|
264
|
+
"tasks_list": _format_tasks_list,
|
|
265
|
+
"builder_result": _format_builder_result,
|
|
269
266
|
}
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
-
import json
|
|
5
4
|
import sys
|
|
6
5
|
from typing import Any, Callable, TextIO
|
|
7
6
|
|
|
8
7
|
from ..errors import QingflowApiError
|
|
8
|
+
from ..ops.base import normalize_exception
|
|
9
|
+
from .commands import register_all_commands
|
|
9
10
|
from .context import CliContext, build_cli_context
|
|
10
11
|
from .formatters import emit_json_result, emit_text_result
|
|
11
|
-
from .commands import register_all_commands
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
def build_parser() -> argparse.ArgumentParser:
|
|
18
|
-
parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI")
|
|
18
|
+
parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI 2.0")
|
|
19
19
|
parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
|
|
20
|
-
parser.add_argument("--json", action="store_true", help="
|
|
20
|
+
parser.add_argument("--json", action="store_true", help="输出结构化 JSON")
|
|
21
21
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
22
22
|
register_all_commands(subparsers)
|
|
23
23
|
return parser
|
|
@@ -46,15 +46,16 @@ def run(
|
|
|
46
46
|
if handler is None:
|
|
47
47
|
parser.print_help(out)
|
|
48
48
|
return 2
|
|
49
|
+
|
|
49
50
|
context = context_factory()
|
|
50
51
|
try:
|
|
51
52
|
result = handler(args, context)
|
|
52
53
|
except RuntimeError as exc:
|
|
53
|
-
|
|
54
|
-
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
54
|
+
result = normalize_exception(exc)
|
|
55
55
|
except QingflowApiError as exc:
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
result = normalize_exception(exc)
|
|
57
|
+
except Exception as exc: # noqa: BLE001
|
|
58
|
+
result = normalize_exception(exc)
|
|
58
59
|
finally:
|
|
59
60
|
context.close()
|
|
60
61
|
|
|
@@ -94,53 +95,17 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
|
94
95
|
return global_args + remaining
|
|
95
96
|
|
|
96
97
|
|
|
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
98
|
def _result_exit_code(result: dict[str, Any]) -> int:
|
|
136
99
|
if not isinstance(result, dict):
|
|
137
100
|
return 0
|
|
138
|
-
if result.get("ok") is False:
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
if
|
|
142
|
-
return
|
|
143
|
-
|
|
101
|
+
if result.get("ok") is not False:
|
|
102
|
+
return 0
|
|
103
|
+
code = str(result.get("code") or "").upper()
|
|
104
|
+
if code in {"AUTH_REQUIRED", "WORKSPACE_NOT_SELECTED"}:
|
|
105
|
+
return 3
|
|
106
|
+
if code in {"INVALID_ARGUMENT", "CONFIG"}:
|
|
107
|
+
return 2
|
|
108
|
+
return 4
|
|
144
109
|
|
|
145
110
|
|
|
146
111
|
if __name__ == "__main__":
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from .base import normalize_exception, success_result
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AppOperations:
|
|
9
|
+
def __init__(self, tools: Any) -> None:
|
|
10
|
+
self._tools = tools
|
|
11
|
+
|
|
12
|
+
def list(self, *, profile: str) -> dict:
|
|
13
|
+
try:
|
|
14
|
+
raw = self._tools.app.app_list(profile=profile)
|
|
15
|
+
except Exception as error: # noqa: BLE001
|
|
16
|
+
return normalize_exception(error)
|
|
17
|
+
items = raw.get("items") if isinstance(raw.get("items"), list) else []
|
|
18
|
+
return success_result(
|
|
19
|
+
"APPS_LISTED",
|
|
20
|
+
"已读取应用列表",
|
|
21
|
+
data={"items": items, "count": raw.get("count", len(items))},
|
|
22
|
+
meta={"profile": profile, "workspace_id": raw.get("ws_id")},
|
|
23
|
+
legacy=raw,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def find(self, *, profile: str, keyword: str, page_num: int, page_size: int) -> dict:
|
|
27
|
+
try:
|
|
28
|
+
raw = self._tools.app.app_search(profile=profile, keyword=keyword, page_num=page_num, page_size=page_size)
|
|
29
|
+
except Exception as error: # noqa: BLE001
|
|
30
|
+
return normalize_exception(error)
|
|
31
|
+
return success_result(
|
|
32
|
+
"APPS_FOUND",
|
|
33
|
+
"已完成应用搜索",
|
|
34
|
+
data={
|
|
35
|
+
"items": raw.get("items") if isinstance(raw.get("items"), list) else [],
|
|
36
|
+
"total": raw.get("total"),
|
|
37
|
+
"keyword": keyword,
|
|
38
|
+
"page_num": page_num,
|
|
39
|
+
"page_size": page_size,
|
|
40
|
+
},
|
|
41
|
+
meta={"profile": profile, "workspace_id": raw.get("ws_id")},
|
|
42
|
+
legacy=raw,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
def show(self, *, profile: str, app_key: str) -> dict:
|
|
46
|
+
try:
|
|
47
|
+
raw = self._tools.app.app_get(profile=profile, app_key=app_key)
|
|
48
|
+
except Exception as error: # noqa: BLE001
|
|
49
|
+
return normalize_exception(error)
|
|
50
|
+
data = raw.get("data") if isinstance(raw.get("data"), dict) else {}
|
|
51
|
+
return success_result(
|
|
52
|
+
"APP_SHOWN",
|
|
53
|
+
"已读取应用信息",
|
|
54
|
+
data={
|
|
55
|
+
"app_key": data.get("app_key", app_key),
|
|
56
|
+
"app_name": data.get("app_name"),
|
|
57
|
+
"can_create": data.get("can_create"),
|
|
58
|
+
"import_capability": data.get("import_capability"),
|
|
59
|
+
"accessible_views": data.get("accessible_views") if isinstance(data.get("accessible_views"), list) else [],
|
|
60
|
+
},
|
|
61
|
+
warnings=raw.get("warnings") if isinstance(raw.get("warnings"), list) else [],
|
|
62
|
+
meta={"profile": profile, "workspace_id": raw.get("ws_id"), "request_route": raw.get("request_route")},
|
|
63
|
+
legacy=raw,
|
|
64
|
+
)
|