@qingflow-tech/qingflow-cli 1.1.48 → 1.1.50
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 -4
- package/npm/lib/runtime.mjs +29 -13
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-cli/SKILL.md +91 -410
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
- package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +13 -5
- package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +5 -3
- package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +1 -1
- package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +1 -1
- package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1665 -1738
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
- package/skills/qingflow-cli/reference/data/README.md +32 -0
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
- package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
- package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +48 -46
- package/skills/qingflow-common/SKILL.md +10 -17
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +142 -12
- package/src/qingflow_mcp/builder_facade/flow_validation.py +569 -0
- package/src/qingflow_mcp/builder_facade/service.py +762 -6
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +155 -3
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/auth.py +1 -1
- package/src/qingflow_mcp/cli/commands/builder.py +57 -41
- package/src/qingflow_mcp/cli/commands/data.py +134 -0
- package/src/qingflow_mcp/cli/commands/record.py +1 -1
- package/src/qingflow_mcp/cli/commands/task.py +10 -12
- package/src/qingflow_mcp/cli/commands/workspace.py +20 -7
- package/src/qingflow_mcp/cli/context.py +4 -1
- package/src/qingflow_mcp/cli/formatters.py +177 -21
- package/src/qingflow_mcp/cli/main.py +4 -1
- package/src/qingflow_mcp/cli/pagination.py +44 -0
- package/src/qingflow_mcp/config.py +60 -0
- package/src/qingflow_mcp/public_surface.py +9 -0
- package/src/qingflow_mcp/resource_uri.py +241 -0
- package/src/qingflow_mcp/response_trim.py +56 -5
- package/src/qingflow_mcp/server.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +57 -2
- package/src/qingflow_mcp/server_app_user.py +10 -3
- package/src/qingflow_mcp/tools/ai_builder_tools.py +374 -41
- package/src/qingflow_mcp/tools/app_tools.py +52 -13
- package/src/qingflow_mcp/tools/csv_export.py +168 -0
- package/src/qingflow_mcp/tools/data_tools.py +284 -0
- package/src/qingflow_mcp/tools/login_link_tools.py +34 -6
- package/src/qingflow_mcp/tools/record_tools.py +85 -61
- package/src/qingflow_mcp/tools/resource_read_tools.py +13 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +21 -4
- package/src/qingflow_mcp/tools/workspace_tools.py +9 -12
|
@@ -8,7 +8,10 @@ from typing import Any, TextIO
|
|
|
8
8
|
def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
|
|
9
9
|
text = _format_cancelled_result(result)
|
|
10
10
|
if text is None:
|
|
11
|
-
if
|
|
11
|
+
if hint in _DATA_FORMAT_HINTS:
|
|
12
|
+
# 数据面失败也必须保留 field_errors 与列表型 hints,不能先走通用失败裁剪。
|
|
13
|
+
text = _format_data_result(result)
|
|
14
|
+
elif _is_failed_result(result) and hint != "task_action_execute":
|
|
12
15
|
text = _format_failed_result(result)
|
|
13
16
|
else:
|
|
14
17
|
formatter = _FORMATTERS.get(hint, _format_generic)
|
|
@@ -90,6 +93,92 @@ def _format_generic(result: dict[str, Any]) -> str:
|
|
|
90
93
|
return "\n".join(lines) + "\n"
|
|
91
94
|
|
|
92
95
|
|
|
96
|
+
def _format_data_result(result: dict[str, Any]) -> str:
|
|
97
|
+
lines = [f"Status: {result.get('status') or ('success' if result.get('ok') is not False else 'failed')}"]
|
|
98
|
+
if result.get("format") == "csv":
|
|
99
|
+
files = result.get("files") if isinstance(result.get("files"), list) else []
|
|
100
|
+
lines.extend(
|
|
101
|
+
[
|
|
102
|
+
f"Local dir: {result.get('local_dir') or '-'}",
|
|
103
|
+
f"Files: {len(files)}",
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
for item in files:
|
|
107
|
+
if isinstance(item, dict):
|
|
108
|
+
lines.append(
|
|
109
|
+
f"- part {item.get('part')}: {item.get('local_path')} ({item.get('row_count')} rows)"
|
|
110
|
+
)
|
|
111
|
+
lines.extend(
|
|
112
|
+
[
|
|
113
|
+
f"Rows: {result.get('row_count')}",
|
|
114
|
+
f"Complete: {result.get('complete')}",
|
|
115
|
+
f"Truncated: {result.get('truncated')}",
|
|
116
|
+
f"Safe for final conclusion: {result.get('safe_for_final_conclusion')}",
|
|
117
|
+
f"Stopped reason: {result.get('stopped_reason') or '-'}",
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
access = result.get("access") if isinstance(result.get("access"), dict) else {}
|
|
121
|
+
if access:
|
|
122
|
+
sources = access.get("scope_sources") if isinstance(access.get("scope_sources"), list) else []
|
|
123
|
+
lines.append(
|
|
124
|
+
f"Access: level={access.get('level') or '-'} / can_write={access.get('can_write')}"
|
|
125
|
+
f" / scope_sources={','.join(str(item) for item in sources) or '-'}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
records = result.get("records") if isinstance(result.get("records"), dict) else {}
|
|
129
|
+
if records:
|
|
130
|
+
items = records.get("items") if isinstance(records.get("items"), list) else []
|
|
131
|
+
lines.append(
|
|
132
|
+
"Pagination: "
|
|
133
|
+
f"page={records.get('page')} / requested_page_size={records.get('requested_page_size')} / "
|
|
134
|
+
f"page_size={records.get('page_size')} / total={records.get('total')} / "
|
|
135
|
+
f"has_more={records.get('has_more')} / returned={len(items)}"
|
|
136
|
+
)
|
|
137
|
+
record = result.get("record") if isinstance(result.get("record"), dict) else {}
|
|
138
|
+
if record:
|
|
139
|
+
lines.append(f"Record ID: {record.get('record_id') or '-'}")
|
|
140
|
+
items = result.get("items") if isinstance(result.get("items"), list) else []
|
|
141
|
+
if items:
|
|
142
|
+
lines.append(f"Items: {len(items)}")
|
|
143
|
+
|
|
144
|
+
for key, label in (
|
|
145
|
+
("error_code", "Error Code"),
|
|
146
|
+
("message", "Message"),
|
|
147
|
+
("write_executed", "Write Executed"),
|
|
148
|
+
("retry_safe", "Retry Safe"),
|
|
149
|
+
):
|
|
150
|
+
if key in result:
|
|
151
|
+
lines.append(f"{label}: {result.get(key)}")
|
|
152
|
+
|
|
153
|
+
field_errors = result.get("field_errors") if isinstance(result.get("field_errors"), list) else []
|
|
154
|
+
if field_errors:
|
|
155
|
+
lines.append("Field Errors:")
|
|
156
|
+
_append_data_field_errors(lines, field_errors)
|
|
157
|
+
for index, item in enumerate(items):
|
|
158
|
+
if not isinstance(item, dict) or not isinstance(item.get("field_errors"), list):
|
|
159
|
+
continue
|
|
160
|
+
lines.append(f"Item {index} Field Errors:")
|
|
161
|
+
_append_data_field_errors(lines, item["field_errors"])
|
|
162
|
+
|
|
163
|
+
hints = result.get("hints") if isinstance(result.get("hints"), list) else []
|
|
164
|
+
lines.append(f"Hints: {len(hints)}")
|
|
165
|
+
for hint in hints:
|
|
166
|
+
if isinstance(hint, dict):
|
|
167
|
+
lines.append(f"- {hint.get('code') or '-'}: {hint.get('message') or '-'}")
|
|
168
|
+
else:
|
|
169
|
+
lines.append(f"- {hint}")
|
|
170
|
+
return "\n".join(lines) + "\n"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _append_data_field_errors(lines: list[str], field_errors: list[Any]) -> None:
|
|
174
|
+
for error in field_errors:
|
|
175
|
+
if isinstance(error, dict):
|
|
176
|
+
lines.append(
|
|
177
|
+
f"- {error.get('path') or '-'}: {error.get('message') or '-'}"
|
|
178
|
+
f" / fix_hint={error.get('fix_hint') or '-'}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
93
182
|
def _format_whoami(result: dict[str, Any]) -> str:
|
|
94
183
|
lines = [
|
|
95
184
|
f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
|
|
@@ -147,7 +236,11 @@ def _format_workspace_list(result: dict[str, Any]) -> str:
|
|
|
147
236
|
str(item.get("remark") or ""),
|
|
148
237
|
]
|
|
149
238
|
)
|
|
150
|
-
|
|
239
|
+
text = _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
|
|
240
|
+
selected = result.get("selected_workspace")
|
|
241
|
+
if isinstance(selected, dict):
|
|
242
|
+
text += f"Selected Workspace: {selected.get('wsId')} / {selected.get('workspaceName') or '-'}\n"
|
|
243
|
+
return text
|
|
151
244
|
|
|
152
245
|
|
|
153
246
|
def _format_workspace_get(result: dict[str, Any]) -> str:
|
|
@@ -191,9 +284,10 @@ def _format_app_items(result: dict[str, Any]) -> str:
|
|
|
191
284
|
str(item.get("app_key") or item.get("appKey") or ""),
|
|
192
285
|
str(item.get("app_name") or item.get("name") or item.get("title") or ""),
|
|
193
286
|
str(item.get("package_name") or item.get("tag_name") or ""),
|
|
287
|
+
str(item.get("uri") or ""),
|
|
194
288
|
]
|
|
195
289
|
)
|
|
196
|
-
return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
|
|
290
|
+
return _render_titled_table("Apps", ["app_key", "app_name", "package", "uri"], rows)
|
|
197
291
|
|
|
198
292
|
|
|
199
293
|
def _format_app_get(result: dict[str, Any]) -> str:
|
|
@@ -201,6 +295,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
201
295
|
lines = [
|
|
202
296
|
f"App: {data.get('app_name') or '-'}",
|
|
203
297
|
f"App Key: {data.get('app_key') or '-'}",
|
|
298
|
+
f"URI: {data.get('uri') or '-'}",
|
|
204
299
|
f"Can Create: {data.get('can_create')}",
|
|
205
300
|
]
|
|
206
301
|
import_capability = data.get("import_capability")
|
|
@@ -224,7 +319,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
224
319
|
lines.append(f"Accessible Views: {len(views)}")
|
|
225
320
|
for item in views[:10]:
|
|
226
321
|
if isinstance(item, dict):
|
|
227
|
-
lines.append(f"- {item.get('view_id')}: {item.get('name')}")
|
|
322
|
+
lines.append(f"- {item.get('view_id')}: {item.get('name')} / {item.get('uri') or '-'}")
|
|
228
323
|
_append_warnings(lines, result.get("warnings"))
|
|
229
324
|
return "\n".join(lines) + "\n"
|
|
230
325
|
|
|
@@ -296,6 +391,7 @@ def _format_view_get(result: dict[str, Any]) -> str:
|
|
|
296
391
|
f"View Key: {data.get('view_key') or '-'}",
|
|
297
392
|
f"App Key: {data.get('app_key') or '-'}",
|
|
298
393
|
f"View Type: {data.get('view_type') or '-'}",
|
|
394
|
+
f"URI: {data.get('uri') or '-'}",
|
|
299
395
|
f"Analysis Supported: {data.get('analysis_supported')}",
|
|
300
396
|
]
|
|
301
397
|
if export_capability:
|
|
@@ -356,11 +452,9 @@ def _format_record_list(result: dict[str, Any]) -> str:
|
|
|
356
452
|
lines.append(f"- returned_count: {lookup.get('returned_count')}")
|
|
357
453
|
lines.append(f"- truncated: {lookup.get('truncated')}")
|
|
358
454
|
lines.append(f"Returned Records: {len(items)}")
|
|
359
|
-
for item in items
|
|
455
|
+
for item in items:
|
|
360
456
|
if isinstance(item, dict):
|
|
361
457
|
lines.append(json.dumps(item, ensure_ascii=False))
|
|
362
|
-
if len(items) > 10:
|
|
363
|
-
lines.append(f"... {len(items) - 10} more")
|
|
364
458
|
_append_warnings(lines, result.get("warnings"))
|
|
365
459
|
_append_verification(lines, result.get("verification"))
|
|
366
460
|
return "\n".join(lines) + "\n"
|
|
@@ -421,10 +515,11 @@ def _format_record_candidates(result: dict[str, Any]) -> str:
|
|
|
421
515
|
f"page_size={pagination.get('page_size')} / "
|
|
422
516
|
f"returned={pagination.get('returned_items')} / "
|
|
423
517
|
f"total={pagination.get('reported_total')} / "
|
|
424
|
-
f"pages={pagination.get('page_amount')}"
|
|
518
|
+
f"pages={pagination.get('page_amount')} / "
|
|
519
|
+
f"has_more={pagination.get('has_more')}"
|
|
425
520
|
)
|
|
426
521
|
lines.append(f"Candidates: {len(items)}")
|
|
427
|
-
for item in items
|
|
522
|
+
for item in items:
|
|
428
523
|
if not isinstance(item, dict):
|
|
429
524
|
lines.append(f"- {item}")
|
|
430
525
|
continue
|
|
@@ -446,8 +541,6 @@ def _format_record_candidates(result: dict[str, Any]) -> str:
|
|
|
446
541
|
if source_labels:
|
|
447
542
|
parts.append("sources=" + ",".join(source_labels))
|
|
448
543
|
lines.append("- " + " / ".join(parts))
|
|
449
|
-
if len(items) > 20:
|
|
450
|
-
lines.append(f"... {len(items) - 20} more")
|
|
451
544
|
_append_warnings(lines, result.get("warnings"))
|
|
452
545
|
_append_verification(lines, result.get("verification"))
|
|
453
546
|
return "\n".join(lines) + "\n"
|
|
@@ -944,11 +1037,9 @@ def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
|
|
|
944
1037
|
result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
|
|
945
1038
|
items = result_payload.get("items") if isinstance(result_payload.get("items"), list) else []
|
|
946
1039
|
lines.append(f"Returned Records: {len(items)}")
|
|
947
|
-
for item in items
|
|
1040
|
+
for item in items:
|
|
948
1041
|
if isinstance(item, dict):
|
|
949
1042
|
lines.append(json.dumps(item, ensure_ascii=False))
|
|
950
|
-
if len(items) > 10:
|
|
951
|
-
lines.append(f"... {len(items) - 10} more")
|
|
952
1043
|
elif result_type == "chart_data":
|
|
953
1044
|
result_payload = data.get("result") if isinstance(data.get("result"), dict) else {}
|
|
954
1045
|
summary = result_payload.get("summary") if isinstance(result_payload.get("summary"), dict) else {}
|
|
@@ -956,11 +1047,9 @@ def _format_task_associated_report_detail(result: dict[str, Any]) -> str:
|
|
|
956
1047
|
if summary:
|
|
957
1048
|
lines.append(f"Summary: {json.dumps(summary, ensure_ascii=False)}")
|
|
958
1049
|
lines.append(f"Rows: {len(rows)}")
|
|
959
|
-
for row in rows
|
|
1050
|
+
for row in rows:
|
|
960
1051
|
if isinstance(row, dict):
|
|
961
1052
|
lines.append(json.dumps(row, ensure_ascii=False))
|
|
962
|
-
if len(rows) > 10:
|
|
963
|
-
lines.append(f"... {len(rows) - 10} more")
|
|
964
1053
|
resolved_filters = context.get("resolved_filters") if isinstance(context.get("resolved_filters"), list) else []
|
|
965
1054
|
if resolved_filters:
|
|
966
1055
|
lines.append(f"Resolved Filters: {len(resolved_filters)}")
|
|
@@ -1175,6 +1264,9 @@ def _format_export_direct(result: dict[str, Any]) -> str:
|
|
|
1175
1264
|
|
|
1176
1265
|
def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
1177
1266
|
lines = []
|
|
1267
|
+
pagination = result.get("pagination")
|
|
1268
|
+
if isinstance(pagination, dict):
|
|
1269
|
+
lines.append("Pagination: " + json.dumps(pagination, ensure_ascii=False))
|
|
1178
1270
|
emitted: set[str] = set()
|
|
1179
1271
|
for key, label in (
|
|
1180
1272
|
("status", "Status"),
|
|
@@ -1184,6 +1276,7 @@ def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
|
1184
1276
|
("view_key", "View Key"),
|
|
1185
1277
|
("chart_id", "Chart ID"),
|
|
1186
1278
|
("package_id", "Package ID"),
|
|
1279
|
+
("uri", "URI"),
|
|
1187
1280
|
("verified", "Verified"),
|
|
1188
1281
|
("write_executed", "Write Executed"),
|
|
1189
1282
|
("delete_executed", "Delete Executed"),
|
|
@@ -1216,16 +1309,27 @@ def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
|
1216
1309
|
"associations",
|
|
1217
1310
|
"package_ids",
|
|
1218
1311
|
"app_keys",
|
|
1312
|
+
"resources",
|
|
1219
1313
|
):
|
|
1220
1314
|
value = result.get(key)
|
|
1221
1315
|
if not isinstance(value, list) and isinstance(data, dict):
|
|
1222
1316
|
value = data.get(key)
|
|
1223
1317
|
if isinstance(value, list):
|
|
1224
1318
|
lines.append(f"{key}: {len(value)}")
|
|
1225
|
-
|
|
1319
|
+
visible_items = value if isinstance(pagination, dict) else value[:8]
|
|
1320
|
+
for item in visible_items:
|
|
1226
1321
|
lines.append("- " + _builder_item_summary(item))
|
|
1227
|
-
if len(value) >
|
|
1228
|
-
lines.append(f"... {len(value) -
|
|
1322
|
+
if len(value) > len(visible_items):
|
|
1323
|
+
lines.append(f"... {len(value) - len(visible_items)} more")
|
|
1324
|
+
if key == "apps":
|
|
1325
|
+
uri_lines = [
|
|
1326
|
+
line
|
|
1327
|
+
for item in value
|
|
1328
|
+
for line in _builder_batch_item_uri_lines(item)
|
|
1329
|
+
]
|
|
1330
|
+
if uri_lines:
|
|
1331
|
+
lines.append("Resource URIs:")
|
|
1332
|
+
lines.extend(f"- {line}" for line in uri_lines)
|
|
1229
1333
|
components = result.get("components") if isinstance(result.get("components"), list) else []
|
|
1230
1334
|
if components:
|
|
1231
1335
|
lines.append("Component Refs:")
|
|
@@ -1294,6 +1398,7 @@ def _builder_item_summary(item: Any) -> str:
|
|
|
1294
1398
|
"uid",
|
|
1295
1399
|
"userId",
|
|
1296
1400
|
"email",
|
|
1401
|
+
"uri",
|
|
1297
1402
|
)
|
|
1298
1403
|
parts = [
|
|
1299
1404
|
f"{key}={item.get(key)}"
|
|
@@ -1305,6 +1410,50 @@ def _builder_item_summary(item: Any) -> str:
|
|
|
1305
1410
|
return " / ".join(parts) if parts else json.dumps(item, ensure_ascii=False)
|
|
1306
1411
|
|
|
1307
1412
|
|
|
1413
|
+
def _builder_batch_item_uri_lines(item: Any) -> list[str]:
|
|
1414
|
+
if not isinstance(item, dict):
|
|
1415
|
+
return []
|
|
1416
|
+
item_status = str(item.get("status") or "").lower()
|
|
1417
|
+
if item_status not in {"success", "partial_success"}:
|
|
1418
|
+
return []
|
|
1419
|
+
data = item.get("data") if isinstance(item.get("data"), dict) else item.get("result")
|
|
1420
|
+
if not isinstance(data, dict) or not _builder_uri_is_confirmed(data, fallback_status=item_status):
|
|
1421
|
+
return []
|
|
1422
|
+
lines: list[str] = []
|
|
1423
|
+
if data.get("uri"):
|
|
1424
|
+
lines.append(f"URI: {data['uri']}")
|
|
1425
|
+
views = data.get("views") if isinstance(data.get("views"), list) else []
|
|
1426
|
+
for view in views:
|
|
1427
|
+
if (
|
|
1428
|
+
isinstance(view, dict)
|
|
1429
|
+
and view.get("uri")
|
|
1430
|
+
and _builder_uri_is_confirmed(view, fallback_status=item_status)
|
|
1431
|
+
):
|
|
1432
|
+
lines.append(f"View URI: {view['uri']}")
|
|
1433
|
+
return lines
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
def _builder_uri_is_confirmed(value: dict[str, Any], *, fallback_status: str) -> bool:
|
|
1437
|
+
status = str(value.get("status") or fallback_status).lower()
|
|
1438
|
+
operation = str(value.get("operation") or "").lower()
|
|
1439
|
+
return (
|
|
1440
|
+
status in {"success", "partial_success", "unchanged", "verified"}
|
|
1441
|
+
and operation
|
|
1442
|
+
not in {
|
|
1443
|
+
"failed",
|
|
1444
|
+
"pending",
|
|
1445
|
+
"planned",
|
|
1446
|
+
"readback_pending",
|
|
1447
|
+
"pending_readback",
|
|
1448
|
+
"unknown",
|
|
1449
|
+
"remove",
|
|
1450
|
+
"removed",
|
|
1451
|
+
"delete",
|
|
1452
|
+
"deleted",
|
|
1453
|
+
}
|
|
1454
|
+
)
|
|
1455
|
+
|
|
1456
|
+
|
|
1308
1457
|
def _format_file_upload_local(result: dict[str, Any]) -> str:
|
|
1309
1458
|
lines = [
|
|
1310
1459
|
f"Upload Kind: {result.get('upload_kind') or result.get('requested_upload_kind') or '-'}",
|
|
@@ -1567,7 +1716,7 @@ def _task_action_debug_lines(result: dict[str, Any]) -> list[str]:
|
|
|
1567
1716
|
|
|
1568
1717
|
|
|
1569
1718
|
def _format_login_link(result: dict[str, Any]) -> str:
|
|
1570
|
-
return f"登录链接:{result['url']}\n过期时间(Unix 毫秒):{result['expires_at']}\n
|
|
1719
|
+
return f"登录链接:{result['url']}\n过期时间(Unix 毫秒):{result['expires_at']}\n链接只能使用一次,请勿泄露;仅在用户要求打开时才打开。\n"
|
|
1571
1720
|
|
|
1572
1721
|
|
|
1573
1722
|
_FORMATTERS = {
|
|
@@ -1609,4 +1758,11 @@ _FORMATTERS = {
|
|
|
1609
1758
|
"builder_summary": _format_builder_summary,
|
|
1610
1759
|
"file_upload_local": _format_file_upload_local,
|
|
1611
1760
|
"feedback_submit": _format_feedback_submit,
|
|
1761
|
+
"data_query": _format_data_result,
|
|
1762
|
+
"data_get": _format_data_result,
|
|
1763
|
+
"data_insert": _format_data_result,
|
|
1764
|
+
"data_update": _format_data_result,
|
|
1765
|
+
"data_delete": _format_data_result,
|
|
1612
1766
|
}
|
|
1767
|
+
|
|
1768
|
+
_DATA_FORMAT_HINTS = {"data_query", "data_get", "data_insert", "data_update", "data_delete"}
|
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from typing import Any, Callable, TextIO
|
|
7
8
|
|
|
8
9
|
from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
|
|
9
10
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
11
|
+
from ..resource_uri import cli_resource_base_url
|
|
10
12
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
13
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
12
14
|
from ..version import get_cli_version, get_cli_version_info
|
|
@@ -94,7 +96,8 @@ def run(
|
|
|
94
96
|
context = context_factory()
|
|
95
97
|
if not bool(args.json):
|
|
96
98
|
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
97
|
-
|
|
99
|
+
with cli_resource_base_url(os.getenv("QINGFLOW_BASE_URL") or ""):
|
|
100
|
+
result = handler(args, context)
|
|
98
101
|
except SystemExit as exc:
|
|
99
102
|
return int(exc.code or 0)
|
|
100
103
|
except RuntimeError as exc:
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
from .commands.common import raise_config_error
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def collect_paginated_items(
|
|
9
|
+
fetch_page: Callable[[int, int], tuple[list[dict[str, Any]], dict[str, Any]]],
|
|
10
|
+
*,
|
|
11
|
+
id_key: str,
|
|
12
|
+
page_size: int = 100,
|
|
13
|
+
) -> list[dict[str, Any]]:
|
|
14
|
+
items: list[dict[str, Any]] = []
|
|
15
|
+
seen: set[str] = set()
|
|
16
|
+
page = 1
|
|
17
|
+
fetched_count = 0
|
|
18
|
+
while True:
|
|
19
|
+
rows, pagination = fetch_page(page, page_size)
|
|
20
|
+
previous_count = len(items)
|
|
21
|
+
for row in rows:
|
|
22
|
+
if not isinstance(row, dict):
|
|
23
|
+
continue
|
|
24
|
+
key = str(row.get(id_key) or "")
|
|
25
|
+
if key and key not in seen:
|
|
26
|
+
seen.add(key)
|
|
27
|
+
items.append(row)
|
|
28
|
+
fetched_count += len(rows)
|
|
29
|
+
page_amount = pagination.get("page_amount")
|
|
30
|
+
total = pagination.get("reported_total")
|
|
31
|
+
if not rows:
|
|
32
|
+
break
|
|
33
|
+
if len(items) == previous_count:
|
|
34
|
+
raise_config_error("Pagination did not advance; the backend returned the same items again.")
|
|
35
|
+
if page_amount is not None:
|
|
36
|
+
if page >= int(page_amount):
|
|
37
|
+
break
|
|
38
|
+
elif total is not None:
|
|
39
|
+
if fetched_count >= int(total):
|
|
40
|
+
break
|
|
41
|
+
elif len(rows) < int(pagination.get("page_size") or page_size):
|
|
42
|
+
break
|
|
43
|
+
page += 1
|
|
44
|
+
return items
|
|
@@ -285,6 +285,66 @@ def get_credit_meter_enabled() -> bool:
|
|
|
285
285
|
return normalized in {"1", "true", "yes", "on"}
|
|
286
286
|
|
|
287
287
|
|
|
288
|
+
# BEGIN temporary flow edit maintenance guard: remove this block when flow editing is restored.
|
|
289
|
+
FLOW_EDIT_MAINTENANCE_NODE_THRESHOLD_DEFAULT = 5
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_flow_edit_maintenance_threshold() -> int | None:
|
|
293
|
+
"""读取流程编辑维护闸门的当前节点数阈值。
|
|
294
|
+
|
|
295
|
+
配置键 flow_edit.maintenance_node_threshold / 环境变量
|
|
296
|
+
QINGFLOW_MCP_FLOW_EDIT_MAINTENANCE_THRESHOLD,默认 5;
|
|
297
|
+
取值 off|disabled|none(大小写不敏感)返回 None 表示闸门关闭
|
|
298
|
+
(评测批量等场景的逃生门);解析失败或负数回退默认值 5。
|
|
299
|
+
"""
|
|
300
|
+
value = get_config_value(
|
|
301
|
+
"flow_edit.maintenance_node_threshold",
|
|
302
|
+
env_var="QINGFLOW_MCP_FLOW_EDIT_MAINTENANCE_THRESHOLD",
|
|
303
|
+
default=FLOW_EDIT_MAINTENANCE_NODE_THRESHOLD_DEFAULT,
|
|
304
|
+
)
|
|
305
|
+
normalized = str(value if value is not None else "").strip().lower()
|
|
306
|
+
if normalized in {"off", "disabled", "none"}:
|
|
307
|
+
return None
|
|
308
|
+
try:
|
|
309
|
+
threshold = int(normalized)
|
|
310
|
+
except ValueError:
|
|
311
|
+
return FLOW_EDIT_MAINTENANCE_NODE_THRESHOLD_DEFAULT
|
|
312
|
+
return threshold if threshold >= 0 else FLOW_EDIT_MAINTENANCE_NODE_THRESHOLD_DEFAULT
|
|
313
|
+
# END temporary flow edit maintenance guard.
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
DEFAULT_HTTP_RETRY_ATTEMPTS = 3
|
|
317
|
+
DEFAULT_HTTP_RETRY_BASE_SECONDS = 3.0
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def get_http_retry_attempts() -> int:
|
|
321
|
+
"""传输层单请求最大尝试次数(含首次),≤1 视为不重试。"""
|
|
322
|
+
value = get_config_value(
|
|
323
|
+
"http.retry_attempts",
|
|
324
|
+
env_var="QINGFLOW_MCP_HTTP_RETRY_ATTEMPTS",
|
|
325
|
+
default=DEFAULT_HTTP_RETRY_ATTEMPTS,
|
|
326
|
+
)
|
|
327
|
+
try:
|
|
328
|
+
parsed = int(value)
|
|
329
|
+
except (ValueError, TypeError):
|
|
330
|
+
return DEFAULT_HTTP_RETRY_ATTEMPTS
|
|
331
|
+
return max(parsed, 1)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def get_http_retry_base_seconds() -> float:
|
|
335
|
+
"""传输层重试线性退避基数:第 n 次失败后等待 base × n 秒。"""
|
|
336
|
+
value = get_config_value(
|
|
337
|
+
"http.retry_base_seconds",
|
|
338
|
+
env_var="QINGFLOW_MCP_HTTP_RETRY_BASE_SECONDS",
|
|
339
|
+
default=DEFAULT_HTTP_RETRY_BASE_SECONDS,
|
|
340
|
+
)
|
|
341
|
+
try:
|
|
342
|
+
parsed = float(value)
|
|
343
|
+
except (ValueError, TypeError):
|
|
344
|
+
return DEFAULT_HTTP_RETRY_BASE_SECONDS
|
|
345
|
+
return parsed if parsed >= 0 else DEFAULT_HTTP_RETRY_BASE_SECONDS
|
|
346
|
+
|
|
347
|
+
|
|
288
348
|
def get_credit_usage_base_url() -> str | None:
|
|
289
349
|
value = get_config_value(
|
|
290
350
|
"credit_meter.apaas.base_url",
|
|
@@ -40,6 +40,11 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
40
40
|
PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
|
|
41
41
|
PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
|
|
42
42
|
PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
|
|
43
|
+
PublicToolSpec(USER_DOMAIN, "data_query", ("data_query",), ("data", "query"), cli_show_effective_context=True),
|
|
44
|
+
PublicToolSpec(USER_DOMAIN, "data_get", ("data_get",), ("data", "get"), cli_show_effective_context=True),
|
|
45
|
+
PublicToolSpec(USER_DOMAIN, "data_insert", ("data_insert",), ("data", "insert"), cli_show_effective_context=True, cli_context_write=True),
|
|
46
|
+
PublicToolSpec(USER_DOMAIN, "data_update", ("data_update",), ("data", "update"), cli_show_effective_context=True, cli_context_write=True),
|
|
47
|
+
PublicToolSpec(USER_DOMAIN, "data_delete", ("data_delete",), ("data", "delete"), cli_show_effective_context=True, cli_context_write=True),
|
|
43
48
|
PublicToolSpec(USER_DOMAIN, "portal_list", ("portal_list",), ("portal", "list"), cli_show_effective_context=True),
|
|
44
49
|
PublicToolSpec(USER_DOMAIN, "portal_get", ("portal_get",), ("portal", "get"), cli_show_effective_context=True),
|
|
45
50
|
PublicToolSpec(USER_DOMAIN, "view_get", ("view_get",), ("view", "get"), cli_show_effective_context=True),
|
|
@@ -128,6 +133,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
128
133
|
PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("builder", "auth", "use-credential"), cli_public=False),
|
|
129
134
|
PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
|
|
130
135
|
PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
|
|
136
|
+
PublicToolSpec(BUILDER_DOMAIN, "get_login_link", ("get_login_link",), mcp_public=True, cli_public=False),
|
|
131
137
|
PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
|
|
132
138
|
PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), ("builder", "workspace", "get"), cli_public=False),
|
|
133
139
|
PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
@@ -168,6 +174,9 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
168
174
|
PublicToolSpec(BUILDER_DOMAIN, "chart_get", ("chart_get",), ("builder", "chart", "get"), has_contract=True, cli_show_effective_context=True),
|
|
169
175
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_get_schema", ("app_flow_get_schema",), ("builder", "flow", "schema"), has_contract=True, cli_show_effective_context=True),
|
|
170
176
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_get", ("app_flow_get",), ("builder", "flow", "get"), has_contract=True, cli_show_effective_context=True),
|
|
177
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_flow_validate", ("app_flow_validate",), ("builder", "flow", "validate"), has_contract=True, cli_show_effective_context=True),
|
|
178
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_flow_list_versions", ("app_flow_list_versions",), ("builder", "flow", "versions"), has_contract=True, cli_show_effective_context=True),
|
|
179
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_flow_restore_version", ("app_flow_restore_version",), ("builder", "flow", "restore"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
171
180
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
172
181
|
PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
173
182
|
PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|