@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.
@@ -2,47 +2,44 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
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
5
+ from ..ops.context import OperationsRuntime, build_operations_runtime
15
6
 
16
7
 
17
8
  @dataclass(slots=True)
18
9
  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
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.backend.close()
41
+ self.runtime.close()
32
42
 
33
43
 
34
44
  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
- )
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: list[str] = []
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
- 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))
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 _format_whoami(result: dict[str, Any]) -> str:
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: {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')}",
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
- 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
- )
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 _format_app_items(result: dict[str, Any]) -> str:
65
- items = result.get("items")
66
- if not isinstance(items, list):
67
- items = result.get("apps")
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 or []:
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 _format_app_get(result: dict[str, Any]) -> str:
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 _format_record_list(result: dict[str, Any]) -> str:
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 _format_task_list(result: dict[str, Any]) -> str:
124
+ def _format_record_show(result: dict[str, Any]) -> str:
120
125
  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")
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"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 '-'}",
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 = result.get("issues") if isinstance(result.get("issues"), list) else []
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: {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 '-'}",
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 _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])
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 emit_json_result(result: dict[str, Any], *, stream: TextIO) -> None:
197
- json.dump(result, stream, ensure_ascii=False, indent=2)
198
- stream.write("\n")
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
- header_line = " ".join(header.ljust(widths[index]) for index, header in enumerate(headers))
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 _dict_scalar_lines(payload: dict[str, Any]) -> list[str]:
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
- 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)}")
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
- "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,
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
- "builder_summary": _format_builder_summary,
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="输出原始 JSON")
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
- payload = _parse_error_payload(exc)
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
- payload = exc.to_dict()
57
- return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
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 4
140
- status = str(result.get("status") or "").lower()
141
- if status in {"failed", "blocked"}:
142
- return 4
143
- return 0
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,3 @@
1
+ from .context import OperationsRuntime, build_operations_runtime
2
+
3
+ __all__ = ["OperationsRuntime", "build_operations_runtime"]
@@ -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
+ )