@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.
Files changed (70) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +29 -13
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/skills/qingflow-cli/SKILL.md +91 -410
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  16. package/skills/qingflow-cli/reference/builder/workflow/03-flow-patterns.md +13 -5
  17. package/skills/qingflow-cli/reference/builder/workflow/07-stage4-validate-spec.md +5 -3
  18. package/skills/qingflow-cli/reference/builder/workflow/08-stage5-apply-verify.md +1 -1
  19. package/skills/qingflow-cli/reference/builder/workflow/11-troubleshooting.md +1 -1
  20. package/skills/qingflow-cli/reference/builder/workflow/workflow-schema.json +1665 -1738
  21. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  22. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  26. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  27. package/skills/qingflow-cli/reference/data/README.md +32 -0
  28. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
  29. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  30. package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
  31. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  32. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  33. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  34. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  35. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
  36. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  37. package/skills/qingflow-cli/scripts/workflow/validate_flow_spec.py +48 -46
  38. package/skills/qingflow-common/SKILL.md +10 -17
  39. package/src/qingflow_mcp/__init__.py +1 -1
  40. package/src/qingflow_mcp/backend_client.py +142 -12
  41. package/src/qingflow_mcp/builder_facade/flow_validation.py +569 -0
  42. package/src/qingflow_mcp/builder_facade/service.py +762 -6
  43. package/src/qingflow_mcp/builder_facade/workflow_spec.py +155 -3
  44. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  45. package/src/qingflow_mcp/cli/commands/auth.py +1 -1
  46. package/src/qingflow_mcp/cli/commands/builder.py +57 -41
  47. package/src/qingflow_mcp/cli/commands/data.py +134 -0
  48. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  49. package/src/qingflow_mcp/cli/commands/task.py +10 -12
  50. package/src/qingflow_mcp/cli/commands/workspace.py +20 -7
  51. package/src/qingflow_mcp/cli/context.py +4 -1
  52. package/src/qingflow_mcp/cli/formatters.py +177 -21
  53. package/src/qingflow_mcp/cli/main.py +4 -1
  54. package/src/qingflow_mcp/cli/pagination.py +44 -0
  55. package/src/qingflow_mcp/config.py +60 -0
  56. package/src/qingflow_mcp/public_surface.py +9 -0
  57. package/src/qingflow_mcp/resource_uri.py +241 -0
  58. package/src/qingflow_mcp/response_trim.py +56 -5
  59. package/src/qingflow_mcp/server.py +2 -0
  60. package/src/qingflow_mcp/server_app_builder.py +57 -2
  61. package/src/qingflow_mcp/server_app_user.py +10 -3
  62. package/src/qingflow_mcp/tools/ai_builder_tools.py +374 -41
  63. package/src/qingflow_mcp/tools/app_tools.py +52 -13
  64. package/src/qingflow_mcp/tools/csv_export.py +168 -0
  65. package/src/qingflow_mcp/tools/data_tools.py +284 -0
  66. package/src/qingflow_mcp/tools/login_link_tools.py +34 -6
  67. package/src/qingflow_mcp/tools/record_tools.py +85 -61
  68. package/src/qingflow_mcp/tools/resource_read_tools.py +13 -0
  69. package/src/qingflow_mcp/tools/task_context_tools.py +21 -4
  70. 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 _is_failed_result(result) and hint != "task_action_execute":
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
- return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
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[:10]:
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[:20]:
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[:10]:
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[:10]:
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
- for item in value[:8]:
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) > 8:
1228
- lines.append(f"... {len(value) - 8} more")
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链接仅可使用一次,请勿泄露。\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
- result = handler(args, context)
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),