@josephyan/qingflow-app-builder-mcp 1.0.9 → 1.1.2
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 +4 -2
- package/npm/bin/qingflow-app-builder-mcp.mjs +33 -2
- package/npm/lib/runtime.mjs +386 -15
- package/npm/scripts/postinstall.mjs +7 -2
- package/package.json +1 -1
- package/pyproject.toml +2 -1
- package/skills/qingflow-app-builder/SKILL.md +135 -14
- package/skills/qingflow-app-builder/references/build-complete-system.md +428 -0
- package/skills/qingflow-app-builder/references/build-single-app.md +530 -0
- package/skills/qingflow-app-builder/references/create-app.md +43 -3
- package/skills/qingflow-app-builder/references/gotchas.md +47 -3
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +25 -10
- package/skills/qingflow-app-builder/references/update-flow.md +29 -1
- package/skills/qingflow-app-builder/references/update-schema.md +6 -2
- package/skills/qingflow-app-builder/references/update-views.md +135 -6
- package/skills/qingflow-mcp-setup/SKILL.md +111 -0
- package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
- package/skills/qingflow-mcp-setup/references/environments.md +61 -0
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +31 -0
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
- package/skills/qingflow-workflow-builder/SKILL.md +96 -0
- package/skills/qingflow-workflow-builder/manifest.yaml +8 -0
- package/skills/qingflow-workflow-builder/references/01-overview.md +45 -0
- package/skills/qingflow-workflow-builder/references/02-update-mode.md +53 -0
- package/skills/qingflow-workflow-builder/references/03-flow-patterns.md +57 -0
- package/skills/qingflow-workflow-builder/references/04-stage1-business-modeling.md +131 -0
- package/skills/qingflow-workflow-builder/references/05-stage2-members-roles.md +29 -0
- package/skills/qingflow-workflow-builder/references/06-stage3-build-spec.md +165 -0
- package/skills/qingflow-workflow-builder/references/07-stage4-validate-spec.md +33 -0
- package/skills/qingflow-workflow-builder/references/08-stage5-apply-verify.md +51 -0
- package/skills/qingflow-workflow-builder/references/09-stage6-summary.md +88 -0
- package/skills/qingflow-workflow-builder/references/10-node-config-reference.md +93 -0
- package/skills/qingflow-workflow-builder/references/11-troubleshooting.md +15 -0
- package/skills/qingflow-workflow-builder/scripts/diff_flow_spec.py +275 -0
- package/skills/qingflow-workflow-builder/scripts/validate_flow_spec.py +605 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +55 -1
- package/src/qingflow_mcp/builder_facade/models.py +532 -48
- package/src/qingflow_mcp/builder_facade/service.py +9194 -2384
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +111 -0
- package/src/qingflow_mcp/cli/commands/app.py +3 -16
- package/src/qingflow_mcp/cli/commands/builder.py +354 -56
- package/src/qingflow_mcp/cli/commands/record.py +112 -6
- package/src/qingflow_mcp/cli/formatters.py +102 -2
- package/src/qingflow_mcp/cli/main.py +245 -3
- package/src/qingflow_mcp/public_surface.py +12 -9
- package/src/qingflow_mcp/response_trim.py +287 -22
- package/src/qingflow_mcp/server.py +29 -20
- package/src/qingflow_mcp/server_app_builder.py +108 -30
- package/src/qingflow_mcp/server_app_user.py +28 -23
- package/src/qingflow_mcp/solution/compiler/__init__.py +1 -3
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
- package/src/qingflow_mcp/solution/executor.py +3 -133
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2617 -440
- package/src/qingflow_mcp/tools/app_tools.py +53 -8
- package/src/qingflow_mcp/tools/package_tools.py +16 -2
- package/src/qingflow_mcp/tools/record_tools.py +14283 -9162
- package/src/qingflow_mcp/tools/resource_read_tools.py +3 -0
- package/src/qingflow_mcp/tools/solution_tools.py +30 -2
- package/src/qingflow_mcp/tools/workflow_tools.py +3 -31
- package/src/qingflow_mcp/version.py +110 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +0 -173
|
@@ -10,7 +10,11 @@ from .common import load_list_arg, load_object_arg, raise_config_error, require_
|
|
|
10
10
|
|
|
11
11
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
12
12
|
parser = subparsers.add_parser("record", help="记录与表结构")
|
|
13
|
-
record_subparsers = parser.add_subparsers(
|
|
13
|
+
record_subparsers = parser.add_subparsers(
|
|
14
|
+
dest="record_command",
|
|
15
|
+
required=True,
|
|
16
|
+
metavar="{schema,list,access,get,logs,insert,update,delete,member-candidates,department-candidates,code-block-run}",
|
|
17
|
+
)
|
|
14
18
|
|
|
15
19
|
schema = record_subparsers.add_parser("schema", help="读取记录相关表结构")
|
|
16
20
|
schema.add_argument("--mode", dest="legacy_mode", help=argparse.SUPPRESS)
|
|
@@ -43,13 +47,37 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
43
47
|
schema_code_block.add_argument("--app-key", required=True)
|
|
44
48
|
schema_code_block.set_defaults(handler=_handle_schema_code_block, format_hint="")
|
|
45
49
|
|
|
50
|
+
member_candidates = record_subparsers.add_parser("member-candidates", help="读取成员字段候选项")
|
|
51
|
+
member_candidates.add_argument("--app-key", required=True)
|
|
52
|
+
member_candidates.add_argument("--field-id", type=int, required=True)
|
|
53
|
+
member_candidates.add_argument("--keyword", default="")
|
|
54
|
+
member_candidates.add_argument("--page-num", type=int, default=1)
|
|
55
|
+
member_candidates.add_argument("--page-size", type=int, default=20)
|
|
56
|
+
member_candidates.add_argument("--record-id")
|
|
57
|
+
member_candidates.add_argument("--workflow-node-id", type=int)
|
|
58
|
+
member_candidates.add_argument("--fields-file")
|
|
59
|
+
member_candidates.set_defaults(handler=_handle_member_candidates, format_hint="")
|
|
60
|
+
|
|
61
|
+
department_candidates = record_subparsers.add_parser("department-candidates", help="读取部门字段候选项")
|
|
62
|
+
department_candidates.add_argument("--app-key", required=True)
|
|
63
|
+
department_candidates.add_argument("--field-id", type=int, required=True)
|
|
64
|
+
department_candidates.add_argument("--keyword", default="")
|
|
65
|
+
department_candidates.add_argument("--page-num", type=int, default=1)
|
|
66
|
+
department_candidates.add_argument("--page-size", type=int, default=20)
|
|
67
|
+
department_candidates.add_argument("--record-id")
|
|
68
|
+
department_candidates.add_argument("--workflow-node-id", type=int)
|
|
69
|
+
department_candidates.add_argument("--fields-file")
|
|
70
|
+
department_candidates.set_defaults(handler=_handle_department_candidates, format_hint="")
|
|
71
|
+
|
|
46
72
|
list_parser = record_subparsers.add_parser("list", help="列出记录")
|
|
47
73
|
list_parser.add_argument("--app-key", required=True)
|
|
48
74
|
list_parser.add_argument("--column", dest="columns", action="append", type=int, default=[])
|
|
49
75
|
list_parser.add_argument("--columns-file")
|
|
76
|
+
list_parser.add_argument("--query")
|
|
77
|
+
list_parser.add_argument("--query-field", dest="query_fields", action="append", type=int, default=[])
|
|
78
|
+
list_parser.add_argument("--query-fields-file")
|
|
50
79
|
list_parser.add_argument("--where-file")
|
|
51
80
|
list_parser.add_argument("--order-by-file")
|
|
52
|
-
list_parser.add_argument("--limit", type=int, default=20)
|
|
53
81
|
list_parser.add_argument("--page", type=int, default=1)
|
|
54
82
|
list_parser.add_argument("--view-id")
|
|
55
83
|
list_parser.add_argument("--list-type", dest="legacy_list_type", type=int, help=argparse.SUPPRESS)
|
|
@@ -72,11 +100,18 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
72
100
|
get.add_argument("--column", dest="columns", action="append", type=int, default=[])
|
|
73
101
|
get.add_argument("--columns-file")
|
|
74
102
|
get.add_argument("--view-id")
|
|
75
|
-
get.set_defaults(handler=_handle_get, format_hint="")
|
|
103
|
+
get.set_defaults(handler=_handle_get, format_hint="record_get")
|
|
104
|
+
|
|
105
|
+
logs = record_subparsers.add_parser("logs", help="读取单条记录全量日志并写入本地 JSONL")
|
|
106
|
+
logs.add_argument("--app-key", required=True)
|
|
107
|
+
logs.add_argument("--record-id", required=True)
|
|
108
|
+
logs.add_argument("--view-id")
|
|
109
|
+
logs.set_defaults(handler=_handle_logs, format_hint="record_logs")
|
|
76
110
|
|
|
77
111
|
insert = record_subparsers.add_parser("insert", help="新增记录")
|
|
78
112
|
insert.add_argument("--app-key", required=True)
|
|
79
|
-
insert.add_argument("--fields-file",
|
|
113
|
+
insert.add_argument("--fields-file", help=argparse.SUPPRESS)
|
|
114
|
+
insert.add_argument("--items-file")
|
|
80
115
|
insert.add_argument("--verify-write", action=argparse.BooleanOptionalAction, default=True)
|
|
81
116
|
insert.set_defaults(handler=_handle_insert, format_hint="")
|
|
82
117
|
|
|
@@ -95,7 +130,12 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
95
130
|
delete.add_argument("--record-ids-file")
|
|
96
131
|
delete.set_defaults(handler=_handle_delete, format_hint="")
|
|
97
132
|
|
|
98
|
-
analyze = record_subparsers.add_parser("analyze", help=
|
|
133
|
+
analyze = record_subparsers.add_parser("analyze", help=argparse.SUPPRESS)
|
|
134
|
+
record_subparsers._choices_actions = [ # type: ignore[attr-defined]
|
|
135
|
+
action
|
|
136
|
+
for action in record_subparsers._choices_actions # type: ignore[attr-defined]
|
|
137
|
+
if getattr(action, "dest", None) != "analyze"
|
|
138
|
+
]
|
|
99
139
|
analyze.add_argument("--app-key", required=True)
|
|
100
140
|
analyze.add_argument("--dimensions-file")
|
|
101
141
|
analyze.add_argument("--metrics-file")
|
|
@@ -131,6 +171,13 @@ def _columns(args: argparse.Namespace) -> list[Any]:
|
|
|
131
171
|
return columns
|
|
132
172
|
|
|
133
173
|
|
|
174
|
+
def _query_fields(args: argparse.Namespace) -> list[Any]:
|
|
175
|
+
query_fields: list[Any] = list(getattr(args, "query_fields", None) or [])
|
|
176
|
+
if getattr(args, "query_fields_file", None):
|
|
177
|
+
query_fields.extend(require_list_arg(args.query_fields_file, option_name="--query-fields-file"))
|
|
178
|
+
return query_fields
|
|
179
|
+
|
|
180
|
+
|
|
134
181
|
def _handle_schema_root(args: argparse.Namespace, _context: CliContext) -> dict:
|
|
135
182
|
mode = (args.legacy_mode or "").strip()
|
|
136
183
|
if mode:
|
|
@@ -188,6 +235,38 @@ def _handle_schema_code_block(args: argparse.Namespace, context: CliContext) ->
|
|
|
188
235
|
return context.code_block.record_code_block_schema_get_public(profile=args.profile, app_key=args.app_key)
|
|
189
236
|
|
|
190
237
|
|
|
238
|
+
def _candidate_context_fields(args: argparse.Namespace) -> dict[str, Any]:
|
|
239
|
+
return load_object_arg(args.fields_file, option_name="--fields-file") or {}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _handle_member_candidates(args: argparse.Namespace, context: CliContext) -> dict:
|
|
243
|
+
return context.record.record_member_candidates(
|
|
244
|
+
profile=args.profile,
|
|
245
|
+
app_key=args.app_key,
|
|
246
|
+
field_id=args.field_id,
|
|
247
|
+
record_id=args.record_id,
|
|
248
|
+
workflow_node_id=args.workflow_node_id,
|
|
249
|
+
fields=_candidate_context_fields(args),
|
|
250
|
+
keyword=args.keyword,
|
|
251
|
+
page_num=args.page_num,
|
|
252
|
+
page_size=args.page_size,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _handle_department_candidates(args: argparse.Namespace, context: CliContext) -> dict:
|
|
257
|
+
return context.record.record_department_candidates(
|
|
258
|
+
profile=args.profile,
|
|
259
|
+
app_key=args.app_key,
|
|
260
|
+
field_id=args.field_id,
|
|
261
|
+
record_id=args.record_id,
|
|
262
|
+
workflow_node_id=args.workflow_node_id,
|
|
263
|
+
fields=_candidate_context_fields(args),
|
|
264
|
+
keyword=args.keyword,
|
|
265
|
+
page_num=args.page_num,
|
|
266
|
+
page_size=args.page_size,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
191
270
|
def _validate_public_view_selector(
|
|
192
271
|
*,
|
|
193
272
|
view_id: str | None,
|
|
@@ -225,9 +304,10 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
225
304
|
profile=args.profile,
|
|
226
305
|
app_key=args.app_key,
|
|
227
306
|
columns=_columns(args),
|
|
307
|
+
query=args.query,
|
|
308
|
+
query_fields=_query_fields(args),
|
|
228
309
|
where=load_list_arg(args.where_file, option_name="--where-file"),
|
|
229
310
|
order_by=load_list_arg(args.order_by_file, option_name="--order-by-file"),
|
|
230
|
-
limit=args.limit,
|
|
231
311
|
page=args.page,
|
|
232
312
|
view_id=args.view_id,
|
|
233
313
|
)
|
|
@@ -254,7 +334,33 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
254
334
|
)
|
|
255
335
|
|
|
256
336
|
|
|
337
|
+
def _handle_logs(args: argparse.Namespace, context: CliContext) -> dict:
|
|
338
|
+
return context.record.record_logs_get(
|
|
339
|
+
profile=args.profile,
|
|
340
|
+
app_key=args.app_key,
|
|
341
|
+
record_id=args.record_id,
|
|
342
|
+
view_id=args.view_id,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
257
346
|
def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
|
|
347
|
+
if args.items_file:
|
|
348
|
+
if args.fields_file:
|
|
349
|
+
raise_config_error(
|
|
350
|
+
"record insert batch mode does not accept --fields-file.",
|
|
351
|
+
fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json` for batch inserts.",
|
|
352
|
+
)
|
|
353
|
+
return context.record.record_insert_public(
|
|
354
|
+
profile=args.profile,
|
|
355
|
+
app_key=args.app_key,
|
|
356
|
+
items=require_list_arg(args.items_file, option_name="--items-file"),
|
|
357
|
+
verify_write=bool(args.verify_write),
|
|
358
|
+
)
|
|
359
|
+
if not args.fields_file:
|
|
360
|
+
raise_config_error(
|
|
361
|
+
"record insert requires --items-file.",
|
|
362
|
+
fix_hint="Use `record insert --app-key APP_KEY --items-file ITEMS.json`; a single insert is one item in the JSON array.",
|
|
363
|
+
)
|
|
258
364
|
return context.record.record_insert_public(
|
|
259
365
|
profile=args.profile,
|
|
260
366
|
app_key=args.app_key,
|
|
@@ -183,7 +183,17 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
183
183
|
def _format_record_list(result: dict[str, Any]) -> str:
|
|
184
184
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
185
185
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
186
|
-
lines = [
|
|
186
|
+
lines: list[str] = []
|
|
187
|
+
lookup = result.get("lookup") if isinstance(result.get("lookup"), dict) else {}
|
|
188
|
+
if lookup:
|
|
189
|
+
lines.append("Lookup:")
|
|
190
|
+
lines.append(f"- query: {lookup.get('query')}")
|
|
191
|
+
lines.append(f"- confidence: {lookup.get('confidence')}")
|
|
192
|
+
lines.append(f"- next_action: {lookup.get('next_action')}")
|
|
193
|
+
lines.append(f"- total_count: {lookup.get('total_count')}")
|
|
194
|
+
lines.append(f"- returned_count: {lookup.get('returned_count')}")
|
|
195
|
+
lines.append(f"- truncated: {lookup.get('truncated')}")
|
|
196
|
+
lines.append(f"Returned Records: {len(items)}")
|
|
187
197
|
for item in items[:10]:
|
|
188
198
|
if isinstance(item, dict):
|
|
189
199
|
lines.append(json.dumps(item, ensure_ascii=False))
|
|
@@ -224,6 +234,95 @@ def _format_record_access(result: dict[str, Any]) -> str:
|
|
|
224
234
|
return "\n".join(lines) + "\n"
|
|
225
235
|
|
|
226
236
|
|
|
237
|
+
def _format_record_get(result: dict[str, Any]) -> str:
|
|
238
|
+
record = result.get("record") if isinstance(result.get("record"), dict) else {}
|
|
239
|
+
app = result.get("app") if isinstance(result.get("app"), dict) else {}
|
|
240
|
+
view = result.get("view") if isinstance(result.get("view"), dict) else {}
|
|
241
|
+
fields = result.get("fields") if isinstance(result.get("fields"), list) else []
|
|
242
|
+
data_logs = result.get("data_logs") if isinstance(result.get("data_logs"), dict) else {}
|
|
243
|
+
workflow_logs = result.get("workflow_logs") if isinstance(result.get("workflow_logs"), dict) else {}
|
|
244
|
+
media_assets = result.get("media_assets") if isinstance(result.get("media_assets"), dict) else {}
|
|
245
|
+
media_items = media_assets.get("items") if isinstance(media_assets.get("items"), list) else []
|
|
246
|
+
downloaded_media = [item for item in media_items if isinstance(item, dict) and item.get("access_status") == "downloaded"]
|
|
247
|
+
failed_media = [item for item in media_items if isinstance(item, dict) and item.get("access_status") != "downloaded"]
|
|
248
|
+
file_assets = result.get("file_assets") if isinstance(result.get("file_assets"), dict) else {}
|
|
249
|
+
file_items = file_assets.get("items") if isinstance(file_assets.get("items"), list) else []
|
|
250
|
+
downloaded_files = [item for item in file_items if isinstance(item, dict) and item.get("access_status") == "downloaded"]
|
|
251
|
+
failed_files = [item for item in file_items if isinstance(item, dict) and item.get("access_status") != "downloaded"]
|
|
252
|
+
extracted_files = [
|
|
253
|
+
item
|
|
254
|
+
for item in downloaded_files
|
|
255
|
+
if isinstance(item.get("extraction"), dict) and item["extraction"].get("status") == "ok"
|
|
256
|
+
]
|
|
257
|
+
associated_resources = result.get("associated_resources") if isinstance(result.get("associated_resources"), list) else []
|
|
258
|
+
unavailable_context = result.get("unavailable_context") if isinstance(result.get("unavailable_context"), list) else []
|
|
259
|
+
lines = [
|
|
260
|
+
f"Status: {result.get('status') or '-'}",
|
|
261
|
+
f"App: {app.get('app_name') or app.get('app_key') or '-'} ({app.get('app_key') or '-'})",
|
|
262
|
+
f"View: {view.get('name') or view.get('view_id') or '-'}",
|
|
263
|
+
f"Record: {record.get('title') or '-'} ({record.get('record_id') or '-'})",
|
|
264
|
+
f"Fields: {len(fields)}",
|
|
265
|
+
f"Data logs: {data_logs.get('status') or '-'} / loaded={data_logs.get('items_loaded')}",
|
|
266
|
+
f"Workflow logs: {workflow_logs.get('status') or '-'} / loaded={workflow_logs.get('items_loaded')}",
|
|
267
|
+
f"Media assets: {media_assets.get('status') or '-'} / downloaded={len(downloaded_media)} / failed={len(failed_media)}",
|
|
268
|
+
f"File assets: {file_assets.get('status') or '-'} / downloaded={len(downloaded_files)} / extracted={len(extracted_files)} / failed={len(failed_files)}",
|
|
269
|
+
f"Associated resources: {len(associated_resources)}",
|
|
270
|
+
f"Unavailable contexts: {len(unavailable_context)}",
|
|
271
|
+
]
|
|
272
|
+
if media_assets.get("local_dir"):
|
|
273
|
+
lines.append(f"Media dir: {media_assets.get('local_dir')}")
|
|
274
|
+
if file_assets.get("local_dir"):
|
|
275
|
+
lines.append(f"File dir: {file_assets.get('local_dir')}")
|
|
276
|
+
if failed_media:
|
|
277
|
+
failure_counts: dict[str, int] = {}
|
|
278
|
+
for item in failed_media:
|
|
279
|
+
key = f"{item.get('access_status') or 'unknown'} via {item.get('download_strategy') or 'unknown'}"
|
|
280
|
+
failure_counts[key] = failure_counts.get(key, 0) + 1
|
|
281
|
+
lines.append("Media failures: " + ", ".join(f"{key}={count}" for key, count in sorted(failure_counts.items())))
|
|
282
|
+
if failed_files:
|
|
283
|
+
failure_counts = {}
|
|
284
|
+
for item in failed_files:
|
|
285
|
+
key = f"{item.get('access_status') or 'unknown'} via {item.get('download_strategy') or 'unknown'}"
|
|
286
|
+
failure_counts[key] = failure_counts.get(key, 0) + 1
|
|
287
|
+
lines.append("File failures: " + ", ".join(f"{key}={count}" for key, count in sorted(failure_counts.items())))
|
|
288
|
+
summary = result.get("summary") if isinstance(result.get("summary"), dict) else {}
|
|
289
|
+
if summary.get("text"):
|
|
290
|
+
lines.append(f"Summary: {summary.get('text')}")
|
|
291
|
+
_append_warnings(lines, result.get("warnings"))
|
|
292
|
+
return "\n".join(lines) + "\n"
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _format_record_logs(result: dict[str, Any]) -> str:
|
|
296
|
+
app = result.get("app") if isinstance(result.get("app"), dict) else {}
|
|
297
|
+
view = result.get("view") if isinstance(result.get("view"), dict) else {}
|
|
298
|
+
record = result.get("record") if isinstance(result.get("record"), dict) else {}
|
|
299
|
+
data_logs = result.get("data_logs") if isinstance(result.get("data_logs"), dict) else {}
|
|
300
|
+
workflow_logs = result.get("workflow_logs") if isinstance(result.get("workflow_logs"), dict) else {}
|
|
301
|
+
integrity = result.get("context_integrity") if isinstance(result.get("context_integrity"), dict) else {}
|
|
302
|
+
lines = [
|
|
303
|
+
f"Status: {result.get('status') or '-'}",
|
|
304
|
+
f"App: {app.get('app_name') or app.get('app_key') or '-'} ({app.get('app_key') or '-'})",
|
|
305
|
+
f"View: {view.get('name') or view.get('view_id') or '-'}",
|
|
306
|
+
f"Record: {record.get('title') or '-'} ({record.get('record_id') or '-'})",
|
|
307
|
+
f"Data logs: {data_logs.get('status') or '-'} / count={data_logs.get('items_count')} / pages={data_logs.get('pages_fetched')} / complete={data_logs.get('complete')}",
|
|
308
|
+
f"Workflow logs: {workflow_logs.get('status') or '-'} / count={workflow_logs.get('items_count')} / pages={workflow_logs.get('pages_fetched')} / complete={workflow_logs.get('complete')}",
|
|
309
|
+
f"Safe for full log conclusion: {integrity.get('safe_for_full_log_conclusion')}",
|
|
310
|
+
]
|
|
311
|
+
if result.get("local_dir"):
|
|
312
|
+
lines.append(f"Local dir: {result.get('local_dir')}")
|
|
313
|
+
if data_logs.get("local_path"):
|
|
314
|
+
lines.append(f"Data logs file: {data_logs.get('local_path')}")
|
|
315
|
+
if workflow_logs.get("local_path"):
|
|
316
|
+
lines.append(f"Workflow logs file: {workflow_logs.get('local_path')}")
|
|
317
|
+
if result.get("summary_path"):
|
|
318
|
+
lines.append(f"Summary file: {result.get('summary_path')}")
|
|
319
|
+
_append_warnings(lines, result.get("warnings"))
|
|
320
|
+
unavailable = result.get("unavailable_context") if isinstance(result.get("unavailable_context"), list) else []
|
|
321
|
+
if unavailable:
|
|
322
|
+
lines.append(f"Unavailable contexts: {len(unavailable)}")
|
|
323
|
+
return "\n".join(lines) + "\n"
|
|
324
|
+
|
|
325
|
+
|
|
227
326
|
def _format_task_list(result: dict[str, Any]) -> str:
|
|
228
327
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
229
328
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
@@ -720,10 +819,11 @@ _FORMATTERS = {
|
|
|
720
819
|
"workspace_get": _format_workspace_get,
|
|
721
820
|
"workspace_select": _format_workspace_select,
|
|
722
821
|
"app_list": _format_app_items,
|
|
723
|
-
"app_search": _format_app_items,
|
|
724
822
|
"app_get": _format_app_get,
|
|
725
823
|
"record_list": _format_record_list,
|
|
726
824
|
"record_access": _format_record_access,
|
|
825
|
+
"record_get": _format_record_get,
|
|
826
|
+
"record_logs": _format_record_logs,
|
|
727
827
|
"task_list": _format_task_list,
|
|
728
828
|
"task_workbench": _format_task_workbench,
|
|
729
829
|
"task_get": _format_task_get,
|
|
@@ -8,6 +8,8 @@ from typing import Any, Callable, TextIO
|
|
|
8
8
|
from ..errors import QingflowApiError
|
|
9
9
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
10
10
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
|
+
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
12
|
+
from ..version import get_cli_version, get_cli_version_info
|
|
11
13
|
from .context import CliContext, build_cli_context
|
|
12
14
|
from .formatters import emit_json_result, emit_text_result
|
|
13
15
|
from .commands import register_all_commands
|
|
@@ -16,11 +18,27 @@ from .commands import register_all_commands
|
|
|
16
18
|
Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
|
|
17
19
|
|
|
18
20
|
|
|
21
|
+
class _CliArgumentError(Exception):
|
|
22
|
+
def __init__(self, *, prog: str, message: str, usage: str) -> None:
|
|
23
|
+
super().__init__(message)
|
|
24
|
+
self.prog = prog
|
|
25
|
+
self.message = message
|
|
26
|
+
self.usage = usage
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _QingflowArgumentParser(argparse.ArgumentParser):
|
|
30
|
+
def error(self, message: str) -> None:
|
|
31
|
+
raise _CliArgumentError(prog=self.prog, message=message, usage=self.format_usage())
|
|
32
|
+
|
|
33
|
+
|
|
19
34
|
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
-
parser =
|
|
35
|
+
parser = _QingflowArgumentParser(prog="qingflow", description="Qingflow CLI")
|
|
21
36
|
parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
|
|
22
37
|
parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
38
|
+
parser.add_argument("--version", action="store_true", help="输出 Qingflow CLI 版本")
|
|
23
39
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
40
|
+
version_parser = subparsers.add_parser("version", help="输出 Qingflow CLI 版本")
|
|
41
|
+
version_parser.set_defaults(handler=_handle_version, format_hint="version")
|
|
24
42
|
register_all_commands(subparsers)
|
|
25
43
|
return parser
|
|
26
44
|
|
|
@@ -40,19 +58,40 @@ def run(
|
|
|
40
58
|
err = stderr or sys.stderr
|
|
41
59
|
parser = build_parser()
|
|
42
60
|
normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
|
|
61
|
+
if "--version" in normalized_argv:
|
|
62
|
+
return _emit_version(json_mode=_should_force_json_output_argv_for_version(normalized_argv), stdout=out)
|
|
43
63
|
try:
|
|
44
64
|
args = parser.parse_args(normalized_argv)
|
|
65
|
+
except _CliArgumentError as exc:
|
|
66
|
+
if _should_force_json_output_argv(normalized_argv):
|
|
67
|
+
payload = {
|
|
68
|
+
"category": "config",
|
|
69
|
+
"status": "failed",
|
|
70
|
+
"error_code": "ARGUMENT_ERROR",
|
|
71
|
+
"message": exc.message,
|
|
72
|
+
"details": {"usage": exc.usage.strip(), "prog": exc.prog},
|
|
73
|
+
}
|
|
74
|
+
payload = _maybe_attach_builder_apply_error_envelope_from_argv(normalized_argv, payload)
|
|
75
|
+
emit_json_result(payload, stream=out)
|
|
76
|
+
return 2
|
|
77
|
+
err.write(exc.usage)
|
|
78
|
+
err.write(f"{exc.prog}: error: {exc.message}\n")
|
|
79
|
+
return 2
|
|
45
80
|
except SystemExit as exc:
|
|
46
81
|
return int(exc.code or 0)
|
|
47
82
|
setattr(args, "_stdin", sys.stdin)
|
|
48
83
|
setattr(args, "_stdout_stream", out)
|
|
49
84
|
setattr(args, "_stderr_stream", err)
|
|
85
|
+
if getattr(args, "command", "") == "version":
|
|
86
|
+
return _emit_version(json_mode=bool(args.json), stdout=out)
|
|
50
87
|
handler = getattr(args, "handler", None)
|
|
51
88
|
if handler is None:
|
|
52
89
|
parser.print_help(out)
|
|
53
90
|
return 2
|
|
54
|
-
context = context_factory()
|
|
55
91
|
try:
|
|
92
|
+
if _should_force_json_output(args):
|
|
93
|
+
setattr(args, "json", True)
|
|
94
|
+
context = context_factory()
|
|
56
95
|
if not bool(args.json):
|
|
57
96
|
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
58
97
|
result = handler(args, context)
|
|
@@ -60,12 +99,15 @@ def run(
|
|
|
60
99
|
return int(exc.code or 0)
|
|
61
100
|
except RuntimeError as exc:
|
|
62
101
|
payload = trim_error_response(_parse_error_payload(exc))
|
|
102
|
+
payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
|
|
63
103
|
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
64
104
|
except QingflowApiError as exc:
|
|
65
105
|
payload = trim_error_response(exc.to_dict())
|
|
106
|
+
payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
|
|
66
107
|
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
67
108
|
finally:
|
|
68
|
-
context
|
|
109
|
+
if "context" in locals():
|
|
110
|
+
context.close()
|
|
69
111
|
|
|
70
112
|
exit_code = _result_exit_code(result)
|
|
71
113
|
trimmed_result = trim_public_response(resolve_cli_tool_name(args), result) if isinstance(result, dict) else result
|
|
@@ -87,6 +129,10 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
|
87
129
|
global_args.append(token)
|
|
88
130
|
index += 1
|
|
89
131
|
continue
|
|
132
|
+
if token == "--version":
|
|
133
|
+
global_args.append(token)
|
|
134
|
+
index += 1
|
|
135
|
+
continue
|
|
90
136
|
if token == "--profile":
|
|
91
137
|
global_args.append(token)
|
|
92
138
|
if index + 1 >= len(argv):
|
|
@@ -104,6 +150,202 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
|
104
150
|
return global_args + remaining
|
|
105
151
|
|
|
106
152
|
|
|
153
|
+
def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
|
|
154
|
+
info = get_cli_version_info()
|
|
155
|
+
return {
|
|
156
|
+
"ok": True,
|
|
157
|
+
"status": "success",
|
|
158
|
+
**info,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
|
|
163
|
+
version = get_cli_version()
|
|
164
|
+
if json_mode:
|
|
165
|
+
emit_json_result(
|
|
166
|
+
{
|
|
167
|
+
"ok": True,
|
|
168
|
+
"status": "success",
|
|
169
|
+
**get_cli_version_info(),
|
|
170
|
+
},
|
|
171
|
+
stream=stdout,
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
stdout.write(f"{version}\n")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
|
|
179
|
+
return "--json" in argv
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _should_force_json_output(args: argparse.Namespace) -> bool:
|
|
183
|
+
if bool(getattr(args, "force_json_output", False)):
|
|
184
|
+
return True
|
|
185
|
+
if (
|
|
186
|
+
getattr(args, "command", "") == "builder"
|
|
187
|
+
and getattr(args, "builder_app_command", "") == "repair-code-blocks"
|
|
188
|
+
and bool(getattr(args, "apply", False))
|
|
189
|
+
):
|
|
190
|
+
return True
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _should_force_json_output_argv(argv: list[str]) -> bool:
|
|
195
|
+
tokens = _strip_global_args(argv)
|
|
196
|
+
if not tokens or tokens[0] not in {"builder", "build"}:
|
|
197
|
+
return False
|
|
198
|
+
if len(tokens) < 3:
|
|
199
|
+
return False
|
|
200
|
+
section = tokens[1]
|
|
201
|
+
action = tokens[2]
|
|
202
|
+
if section in {"package", "button", "associated-resource", "associated-resources", "portal", "schema", "layout", "views", "flow", "charts"}:
|
|
203
|
+
return action == "apply"
|
|
204
|
+
if section == "publish":
|
|
205
|
+
return action == "verify"
|
|
206
|
+
if section == "app":
|
|
207
|
+
if action == "release-edit-lock-if-mine":
|
|
208
|
+
return True
|
|
209
|
+
if action == "repair-code-blocks":
|
|
210
|
+
return "--apply" in tokens
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _maybe_attach_builder_apply_error_envelope_from_args(args: argparse.Namespace, payload: dict[str, Any]) -> dict[str, Any]:
|
|
215
|
+
operation = _builder_apply_operation_from_args(args)
|
|
216
|
+
if not operation:
|
|
217
|
+
return payload
|
|
218
|
+
enriched = dict(payload)
|
|
219
|
+
enriched.setdefault("status", "failed")
|
|
220
|
+
enriched.setdefault("ok", False)
|
|
221
|
+
enriched.setdefault("write_executed", False)
|
|
222
|
+
enriched.setdefault("safe_to_retry", False)
|
|
223
|
+
_copy_arg_identity(enriched, args)
|
|
224
|
+
return _attach_builder_apply_envelope(operation, enriched)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _maybe_attach_builder_apply_error_envelope_from_argv(argv: list[str], payload: dict[str, Any]) -> dict[str, Any]:
|
|
228
|
+
operation = _builder_apply_operation_from_argv(argv)
|
|
229
|
+
if not operation:
|
|
230
|
+
return payload
|
|
231
|
+
enriched = dict(payload)
|
|
232
|
+
enriched.setdefault("status", "failed")
|
|
233
|
+
enriched.setdefault("ok", False)
|
|
234
|
+
enriched.setdefault("write_executed", False)
|
|
235
|
+
enriched.setdefault("safe_to_retry", False)
|
|
236
|
+
_copy_argv_identity(enriched, argv)
|
|
237
|
+
return _attach_builder_apply_envelope(operation, enriched)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
|
|
241
|
+
if getattr(args, "command", "") not in {"builder", "build"}:
|
|
242
|
+
return None
|
|
243
|
+
section = str(getattr(args, "builder_command", "") or "")
|
|
244
|
+
if section == "package" and getattr(args, "builder_package_command", "") == "apply":
|
|
245
|
+
return "package_apply"
|
|
246
|
+
if section == "button" and getattr(args, "builder_button_command", "") == "apply":
|
|
247
|
+
return "app_custom_buttons_apply"
|
|
248
|
+
if section in {"associated-resource", "associated-resources"} and getattr(args, "builder_associated_resource_command", "") == "apply":
|
|
249
|
+
return "app_associated_resources_apply"
|
|
250
|
+
if section == "portal" and getattr(args, "builder_portal_command", "") == "apply":
|
|
251
|
+
return "portal_apply"
|
|
252
|
+
if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
|
|
253
|
+
return "app_schema_apply"
|
|
254
|
+
if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
|
|
255
|
+
return "app_layout_apply"
|
|
256
|
+
if section == "views" and getattr(args, "builder_views_command", "") == "apply":
|
|
257
|
+
return "app_views_apply"
|
|
258
|
+
if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
|
|
259
|
+
return "app_flow_apply"
|
|
260
|
+
if section == "charts" and getattr(args, "builder_charts_command", "") == "apply":
|
|
261
|
+
return "app_charts_apply"
|
|
262
|
+
if section == "publish" and getattr(args, "builder_publish_command", "") == "verify":
|
|
263
|
+
return "app_publish_verify"
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
|
|
268
|
+
tokens = _strip_global_args(argv)
|
|
269
|
+
if not tokens or tokens[0] not in {"builder", "build"} or len(tokens) < 3:
|
|
270
|
+
return None
|
|
271
|
+
section = tokens[1]
|
|
272
|
+
action = tokens[2]
|
|
273
|
+
if action != "apply" and not (section == "publish" and action == "verify"):
|
|
274
|
+
return None
|
|
275
|
+
mapping = {
|
|
276
|
+
"package": "package_apply",
|
|
277
|
+
"button": "app_custom_buttons_apply",
|
|
278
|
+
"associated-resource": "app_associated_resources_apply",
|
|
279
|
+
"associated-resources": "app_associated_resources_apply",
|
|
280
|
+
"portal": "portal_apply",
|
|
281
|
+
"schema": "app_schema_apply",
|
|
282
|
+
"layout": "app_layout_apply",
|
|
283
|
+
"views": "app_views_apply",
|
|
284
|
+
"flow": "app_flow_apply",
|
|
285
|
+
"charts": "app_charts_apply",
|
|
286
|
+
"publish": "app_publish_verify",
|
|
287
|
+
}
|
|
288
|
+
return mapping.get(section)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _copy_arg_identity(payload: dict[str, Any], args: argparse.Namespace) -> None:
|
|
292
|
+
for attr, key in (
|
|
293
|
+
("app_key", "app_key"),
|
|
294
|
+
("app_name", "app_name"),
|
|
295
|
+
("app_title", "app_title"),
|
|
296
|
+
("package_id", "package_id"),
|
|
297
|
+
("dash_key", "dash_key"),
|
|
298
|
+
("dash_name", "dash_name"),
|
|
299
|
+
):
|
|
300
|
+
value = getattr(args, attr, None)
|
|
301
|
+
if value not in (None, ""):
|
|
302
|
+
payload.setdefault(key, value)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _copy_argv_identity(payload: dict[str, Any], argv: list[str]) -> None:
|
|
306
|
+
tokens = _strip_global_args(argv)
|
|
307
|
+
option_to_key = {
|
|
308
|
+
"--app-key": "app_key",
|
|
309
|
+
"--app-name": "app_name",
|
|
310
|
+
"--app-title": "app_title",
|
|
311
|
+
"--package-id": "package_id",
|
|
312
|
+
"--dash-key": "dash_key",
|
|
313
|
+
"--dash-name": "dash_name",
|
|
314
|
+
}
|
|
315
|
+
index = 0
|
|
316
|
+
while index < len(tokens):
|
|
317
|
+
token = tokens[index]
|
|
318
|
+
if token in option_to_key and index + 1 < len(tokens):
|
|
319
|
+
payload.setdefault(option_to_key[token], tokens[index + 1])
|
|
320
|
+
index += 2
|
|
321
|
+
continue
|
|
322
|
+
for option, key in option_to_key.items():
|
|
323
|
+
prefix = f"{option}="
|
|
324
|
+
if token.startswith(prefix):
|
|
325
|
+
payload.setdefault(key, token[len(prefix) :])
|
|
326
|
+
break
|
|
327
|
+
index += 1
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _strip_global_args(argv: list[str]) -> list[str]:
|
|
331
|
+
stripped: list[str] = []
|
|
332
|
+
index = 0
|
|
333
|
+
while index < len(argv):
|
|
334
|
+
token = argv[index]
|
|
335
|
+
if token == "--json":
|
|
336
|
+
index += 1
|
|
337
|
+
continue
|
|
338
|
+
if token == "--profile":
|
|
339
|
+
index += 2
|
|
340
|
+
continue
|
|
341
|
+
if token.startswith("--profile="):
|
|
342
|
+
index += 1
|
|
343
|
+
continue
|
|
344
|
+
stripped.append(token)
|
|
345
|
+
index += 1
|
|
346
|
+
return stripped
|
|
347
|
+
|
|
348
|
+
|
|
107
349
|
def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
|
|
108
350
|
raw = str(exc)
|
|
109
351
|
try:
|