@qingflow-tech/qingflow-cli 1.1.49 → 1.1.50-beta.1

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 (64) 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 +92 -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/backend_client.py +142 -12
  40. package/src/qingflow_mcp/builder_facade/flow_validation.py +569 -0
  41. package/src/qingflow_mcp/builder_facade/service.py +749 -6
  42. package/src/qingflow_mcp/builder_facade/workflow_spec.py +154 -2
  43. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  44. package/src/qingflow_mcp/cli/commands/auth.py +1 -1
  45. package/src/qingflow_mcp/cli/commands/builder.py +57 -41
  46. package/src/qingflow_mcp/cli/commands/data.py +134 -0
  47. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  48. package/src/qingflow_mcp/cli/context.py +4 -1
  49. package/src/qingflow_mcp/cli/formatters.py +159 -4
  50. package/src/qingflow_mcp/cli/main.py +4 -1
  51. package/src/qingflow_mcp/config.py +60 -0
  52. package/src/qingflow_mcp/public_surface.py +9 -0
  53. package/src/qingflow_mcp/resource_uri.py +241 -0
  54. package/src/qingflow_mcp/response_trim.py +53 -5
  55. package/src/qingflow_mcp/server.py +2 -0
  56. package/src/qingflow_mcp/server_app_builder.py +57 -2
  57. package/src/qingflow_mcp/server_app_user.py +10 -3
  58. package/src/qingflow_mcp/tools/ai_builder_tools.py +374 -41
  59. package/src/qingflow_mcp/tools/app_tools.py +52 -13
  60. package/src/qingflow_mcp/tools/csv_export.py +168 -0
  61. package/src/qingflow_mcp/tools/data_tools.py +284 -0
  62. package/src/qingflow_mcp/tools/login_link_tools.py +34 -6
  63. package/src/qingflow_mcp/tools/record_tools.py +11 -46
  64. package/src/qingflow_mcp/tools/resource_read_tools.py +13 -0
@@ -8,6 +8,11 @@ from ..json_types import JSONObject, JSONValue
8
8
  WORKFLOW_SPEC_SCHEMA_PATH = "/api/workflow/spec/schema"
9
9
  WORKFLOW_SPEC_GET_PATH = "/api/workflow/spec"
10
10
  WORKFLOW_SPEC_APPLY_PATH = "/api/workflow/spec:apply"
11
+ # 版本工具化(spec-15 Phase B / #173):契约 5 预留路径,与后端 spec-17 端点对齐。
12
+ # 后端 GET versions / POST :restore 端点由 #165 交付;桥接层按下列路径调用,
13
+ # 若后端实际路径有出入,只需改这里的两个常量。
14
+ WORKFLOW_SPEC_VERSIONS_PATH = "/api/workflow/spec/versions"
15
+ WORKFLOW_SPEC_RESTORE_PATH = "/api/workflow/spec:restore"
11
16
  DEFAULT_SCHEMA_VERSION = "vnext-2026-06"
12
17
 
13
18
 
@@ -50,19 +55,146 @@ def post_apply(
50
55
  return backend.request("POST", context, WORKFLOW_SPEC_APPLY_PATH, json_body=apply_body)
51
56
 
52
57
 
58
+ CONFIRMATION_REQUIRED_STATUS = "CONFIRMATION_REQUIRED"
59
+
60
+
61
+ def is_confirmation_required(apply_result: JSONValue) -> bool:
62
+ """True when the backend gated a destructive apply behind confirmation (two-phase protocol)."""
63
+ return isinstance(apply_result, dict) and str(apply_result.get("status") or "").upper() == CONFIRMATION_REQUIRED_STATUS
64
+
65
+
66
+ def confirmation_required_summary(apply_result: JSONValue) -> JSONObject:
67
+ """Structured, agent-consumable digest of a CONFIRMATION_REQUIRED response.
68
+
69
+ Carries the confirm ticket, expiry, reason code, per-node create/update/delete counts and
70
+ unrebuildable attachment warnings. Never raises: missing backend fields degrade to defaults.
71
+ """
72
+ if not isinstance(apply_result, dict):
73
+ return {}
74
+ preview = apply_result.get("preview") if isinstance(apply_result.get("preview"), dict) else {}
75
+ nodes = preview.get("nodes") if isinstance(preview.get("nodes"), list) else []
76
+ counts = {"create": 0, "update": 0, "delete": 0}
77
+ unrecoverable_warnings: list[str] = []
78
+ replacement_hints: list[JSONObject] = []
79
+ for node in nodes:
80
+ if not isinstance(node, dict):
81
+ continue
82
+ action = str(node.get("action") or "").lower()
83
+ if action in counts:
84
+ counts[action] += 1
85
+ if action == "delete" and node.get("warning"):
86
+ unrecoverable_warnings.append(str(node.get("warning")))
87
+ if action == "create" and isinstance(node.get("possibleReplacementOf"), list) and node.get("possibleReplacementOf"):
88
+ replacement_hints.append(
89
+ {
90
+ "spec_id": node.get("specId"),
91
+ "candidates": [
92
+ {
93
+ "audit_node_id": item.get("auditNodeId"),
94
+ "name": item.get("name"),
95
+ "hint": item.get("hint"),
96
+ }
97
+ for item in node.get("possibleReplacementOf", [])
98
+ if isinstance(item, dict)
99
+ ],
100
+ }
101
+ )
102
+ backend_summary = preview.get("summary") if isinstance(preview.get("summary"), dict) else {}
103
+ return {
104
+ "confirm_ticket": apply_result.get("confirmTicket"),
105
+ "ticket_expires_at": apply_result.get("ticketExpiresAt"),
106
+ "reason_code": apply_result.get("reasonCode"),
107
+ "message": apply_result.get("message"),
108
+ "counts": {
109
+ "create": counts["create"],
110
+ "update": counts["update"],
111
+ "delete": counts["delete"],
112
+ "delete_with_unrecoverable_configs": int(
113
+ backend_summary.get("deleteWithUnrecoverableConfigs") or 0
114
+ ),
115
+ },
116
+ "unrecoverable_warnings": unrecoverable_warnings,
117
+ "id_contract_replacements": replacement_hints,
118
+ }
119
+
120
+ def fetch_versions(
121
+ backend: BackendClient,
122
+ context: BackendRequestContext,
123
+ *,
124
+ app_key: str,
125
+ ) -> JSONValue:
126
+ """GET /api/workflow/spec/versions?appKey=X —— 版本快照列表。"""
127
+ return backend.request(
128
+ "GET",
129
+ context,
130
+ WORKFLOW_SPEC_VERSIONS_PATH,
131
+ params={"appKey": app_key},
132
+ )
133
+
134
+
135
+ def post_restore(
136
+ backend: BackendClient,
137
+ context: BackendRequestContext,
138
+ *,
139
+ restore_body: JSONObject,
140
+ ) -> JSONValue:
141
+ """POST /api/workflow/spec:restore —— 回滚到指定版本快照。"""
142
+ return backend.request("POST", context, WORKFLOW_SPEC_RESTORE_PATH, json_body=restore_body)
143
+
144
+
145
+ def normalize_version_entries(payload: JSONValue) -> list[JSONObject]:
146
+ """把版本列表响应归一成条目数组。
147
+
148
+ 兼容三种后端形态:裸数组 / {"items": [...]} / {"versions": [...]}。
149
+ 条目字段保持后端 camelCase 原样(versionId/snapshotType/createdAt/...),
150
+ 由 facade 层再做 snake_case 化的稳定输出。
151
+ """
152
+ if isinstance(payload, list):
153
+ entries = payload
154
+ elif isinstance(payload, dict):
155
+ raw = payload.get("items")
156
+ if not isinstance(raw, list):
157
+ raw = payload.get("versions")
158
+ if isinstance(raw, list):
159
+ entries = raw
160
+ elif all(not isinstance(value, (list, dict)) for value in payload.values()) and payload:
161
+ # 单条目对象(如 {versionId: ...})——包装成单元素列表
162
+ entries = [payload]
163
+ else:
164
+ entries = []
165
+ else:
166
+ entries = []
167
+ return [entry for entry in entries if isinstance(entry, dict)]
168
+
169
+
53
170
  def verify_apply_response(
54
171
  *,
55
172
  input_spec: JSONObject,
56
173
  apply_result: JSONObject,
57
174
  ) -> tuple[JSONObject, list[dict[str, Any]], bool]:
58
175
  warnings: list[dict[str, Any]] = []
176
+ if is_confirmation_required(apply_result):
177
+ # Two-phase protocol: nothing was applied yet; this is a structured signal, not a
178
+ # verification failure. The caller decides whether to re-invoke with the confirm ticket.
179
+ return (
180
+ {
181
+ "applied_spec_available": False,
182
+ "confirmation_required": True,
183
+ "confirmation_summary": confirmation_required_summary(apply_result),
184
+ },
185
+ warnings,
186
+ False,
187
+ )
59
188
  applied_spec = apply_result.get("appliedSpec")
60
189
  diff_summary = apply_result.get("diffSummary")
61
190
  semantic_lint = apply_result.get("semanticLint") if isinstance(apply_result.get("semanticLint"), list) else []
62
191
 
63
192
  verification: JSONObject = {
64
193
  "applied_spec_available": isinstance(applied_spec, dict),
65
- "version_snapshot_confirmed": False,
194
+ # 后端 apply 两条路径(新版本持久化 / idempotencyKey 幂等命中)都会
195
+ # setCurrentVersionId;该字段真实反映「post_apply 版本快照已落库」
196
+ # (#162 D4:原先初始化 False 后无任何置 True 路径,恒为噪声)。
197
+ "version_snapshot_confirmed": bool(apply_result.get("currentVersionId")),
66
198
  }
67
199
 
68
200
  lint_errors = [
@@ -82,6 +214,7 @@ def verify_apply_response(
82
214
  )
83
215
 
84
216
  diff_failed = False
217
+ missing_input_ids: list[str] = []
85
218
  input_ids: set[str] = set()
86
219
  if isinstance(input_spec, dict):
87
220
  input_ids = {
@@ -106,6 +239,25 @@ def verify_apply_response(
106
239
  missing_input_ids = sorted(node_id for node_id in input_ids if node_id not in applied_ids)
107
240
  if missing_input_ids and not diff_failed:
108
241
  verification["missing_input_node_ids"] = missing_input_ids
242
+ # 节点静默丢失被报 success 的代价(agent 误信继续编排)远大于首次
243
+ # apply 误报 partial_success 的代价(多一次 get,无害);missing
244
+ # 收编进 verified(#162 D3:verified=False → status=partial_success)。
245
+ warnings.append(
246
+ {
247
+ "code": "MISSING_INPUT_NODE",
248
+ "message": (
249
+ "submitted nodes missing from appliedSpec; if this is the first apply "
250
+ "of a new flow, node ids are reassigned by backend — run app_flow_get "
251
+ "to read back"
252
+ ),
253
+ "path": None,
254
+ }
255
+ )
109
256
 
110
- verified = not lint_errors and not diff_failed and verification.get("applied_spec_available") is True
257
+ verified = (
258
+ not lint_errors
259
+ and not diff_failed
260
+ and verification.get("applied_spec_available") is True
261
+ and not missing_input_ids
262
+ )
111
263
  return verification, warnings, verified
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
 
5
- from . import app, auth, builder, chart, exports, imports, portal, record, task, view, workspace
5
+ from . import app, auth, builder, chart, data, exports, imports, portal, record, task, view, workspace
6
6
 
7
7
 
8
8
  def register_all_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -13,6 +13,7 @@ def register_all_commands(subparsers: argparse._SubParsersAction[argparse.Argume
13
13
  view.register(subparsers)
14
14
  chart.register(subparsers)
15
15
  record.register(subparsers)
16
+ data.register(subparsers)
16
17
  imports.register(subparsers)
17
18
  exports.register(subparsers)
18
19
  task.register(subparsers)
@@ -41,7 +41,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
41
41
  whoami.set_defaults(handler=_handle_whoami, format_hint="auth_whoami")
42
42
 
43
43
  login_link = auth_subparsers.add_parser("login-link", help="生成当前工作区的临时一次性登录链接(不自动打开)")
44
- login_link.add_argument("--redirect-path", default="", help="登录后跳转的站内路径,可包含查询参数和锚点")
44
+ login_link.add_argument("--redirect-path", default="", help="登录后跳转的站内路径;资源路径必须属于当前工作区,其他业务路径可包含查询参数和锚点")
45
45
  login_link.set_defaults(handler=_handle_login_link, format_hint="login_link")
46
46
 
47
47
  logout = auth_subparsers.add_parser("logout", help="退出登录")
@@ -23,11 +23,6 @@ from ..json_io import load_json_value
23
23
  from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
24
24
 
25
25
 
26
- # BEGIN temporary flow edit maintenance guard: remove this block when flow editing is restored.
27
- FLOW_EDIT_MAINTENANCE_NODE_THRESHOLD = 5
28
- # END temporary flow edit maintenance guard.
29
-
30
-
31
26
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
32
27
  parser = subparsers.add_parser("builder", aliases=["build"], help="稳定 builder 命令")
33
28
  builder_subparsers = parser.add_subparsers(dest="builder_command", required=True)
@@ -318,8 +313,32 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
318
313
  flow_apply_apply.add_argument("--patch-nodes-file", help="节点局部更新 JSON 数组,每项含 id + set/unset")
319
314
  flow_apply_apply.add_argument("--idempotency-key", default="")
320
315
  flow_apply_apply.add_argument("--schema-version", default="")
316
+ flow_apply_apply.add_argument(
317
+ "--confirm-ticket",
318
+ default="",
319
+ help="破坏性流程变更的两段式确认票据:首次 apply 返回 status=confirmation_required 时,"
320
+ "携同一份 spec + 此票据重新调用方可执行;"
321
+ "票据一次性且有时效,spec 与草稿基线漂移后需重新预览取新票据",
322
+ )
323
+ flow_apply_apply.add_argument("--base-version", type=int, default=None, help="乐观锁基线版本号(builder flow get 响应的 draft_version);提供后草稿被并发修改时 apply 返回冲突而非覆盖")
321
324
  flow_apply_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary", force_json_output=True)
322
325
 
326
+ flow_validate = flow_apply_subparsers.add_parser("validate", help="离线校验 WorkflowSpec,不写后端")
327
+ flow_validate.add_argument("--file", required=True, help="WorkflowSpec JSON 文件,- 表示 stdin")
328
+ flow_validate.add_argument("--schema-version", default="", help="固定使用的 WorkflowSpec Schema 版本,缺省用后端当前版本")
329
+ flow_validate.set_defaults(handler=_handle_flow_validate, format_hint="builder_summary", force_json_output=True)
330
+
331
+ flow_versions = flow_apply_subparsers.add_parser("versions", help="列出 WorkflowSpec 版本快照")
332
+ flow_versions.add_argument("--app-key", required=True)
333
+ flow_versions.set_defaults(handler=_handle_flow_versions, format_hint="builder_summary")
334
+
335
+ flow_restore = flow_apply_subparsers.add_parser("restore", help="回滚到指定版本快照")
336
+ flow_restore.add_argument("--app-key", required=True)
337
+ flow_restore.add_argument("--version-id", required=True)
338
+ flow_restore.add_argument("--idempotency-key", default="")
339
+ flow_restore.add_argument("--schema-version", default="")
340
+ flow_restore.set_defaults(handler=_handle_flow_restore, format_hint="builder_summary", force_json_output=True)
341
+
323
342
  charts_apply = builder_subparsers.add_parser("charts", help="报表")
324
343
  charts_apply_subparsers = charts_apply.add_subparsers(dest="builder_charts_command", required=True)
325
344
  charts_apply_apply = charts_apply_subparsers.add_parser("apply", help="执行报表变更")
@@ -1264,30 +1283,7 @@ def _handle_flow_apply(args: argparse.Namespace, context: CliContext) -> dict:
1264
1283
  "builder flow apply requires exactly one input mode: --spec-file or --patch-nodes-file.",
1265
1284
  fix_hint="Use --spec-file for a complete WorkflowSpec, or --patch-nodes-file for targeted edits after builder flow get.",
1266
1285
  )
1267
- # BEGIN temporary flow edit maintenance guard: remove this block when flow editing is restored.
1268
- current_flow = context.builder.app_flow_get(profile=args.profile, app_key=args.app_key)
1269
- if str(current_flow.get("status") or "").lower() != "success":
1270
- return current_flow
1271
- current_node_count = _flow_spec_node_count(current_flow)
1272
- if current_node_count is None:
1273
- raise_config_error(
1274
- "无法读取当前流程节点,未执行流程变更。",
1275
- fix_hint="当前流程状态无法确认,未执行流程变更。请稍后重试。",
1276
- error_code="FLOW_STATE_UNAVAILABLE",
1277
- details={"app_key": args.app_key},
1278
- )
1279
- if current_node_count > FLOW_EDIT_MAINTENANCE_NODE_THRESHOLD:
1280
- raise_config_error(
1281
- "功能维护中,暂时不允许处理",
1282
- fix_hint="当前流程节点超过 5 个,流程编辑功能维护中;请稍后重试。",
1283
- error_code="FLOW_EDIT_UNDER_MAINTENANCE",
1284
- details={
1285
- "app_key": args.app_key,
1286
- "current_node_count": current_node_count,
1287
- "maintenance_node_threshold": FLOW_EDIT_MAINTENANCE_NODE_THRESHOLD,
1288
- },
1289
- )
1290
- # END temporary flow edit maintenance guard.
1286
+ # 维护闸门已下沉到 facade 层(CLI/MCP/进程内评测统一判定),CLI 不再做前置读。
1291
1287
  if patch_nodes:
1292
1288
  return context.builder.app_flow_apply(
1293
1289
  profile=args.profile,
@@ -1305,6 +1301,8 @@ def _handle_flow_apply(args: argparse.Namespace, context: CliContext) -> dict:
1305
1301
  spec=spec,
1306
1302
  idempotency_key=args.idempotency_key,
1307
1303
  schema_version=args.schema_version,
1304
+ confirm_ticket=args.confirm_ticket or None,
1305
+ base_version=getattr(args, "base_version", None),
1308
1306
  )
1309
1307
  raise_config_error(
1310
1308
  "builder flow apply requires --spec-file or --patch-nodes-file.",
@@ -1312,26 +1310,44 @@ def _handle_flow_apply(args: argparse.Namespace, context: CliContext) -> dict:
1312
1310
  )
1313
1311
 
1314
1312
 
1315
- # BEGIN temporary flow edit maintenance guard: remove this helper when flow editing is restored.
1316
- def _flow_spec_node_count(flow_result: dict) -> int | None:
1317
- spec = flow_result.get("spec")
1318
- if not isinstance(spec, dict):
1319
- return None
1320
- nodes = spec.get("nodes")
1321
- return len(nodes) if isinstance(nodes, list) else None
1322
-
1323
-
1324
- # END temporary flow edit maintenance guard.
1325
-
1326
-
1327
1313
  def _handle_flow_schema(args: argparse.Namespace, context: CliContext) -> dict:
1328
1314
  return context.builder.app_flow_get_schema(profile=args.profile, schema_version=args.schema_version)
1329
1315
 
1330
1316
 
1317
+ def _handle_flow_validate(args: argparse.Namespace, context: CliContext) -> dict:
1318
+ payload = load_json_value(
1319
+ args.file,
1320
+ option_name="--file",
1321
+ max_bytes=4 * 1024 * 1024,
1322
+ max_depth=64,
1323
+ )
1324
+ if not isinstance(payload, dict):
1325
+ raise_config_error("builder flow validate --file must contain a JSON object")
1326
+ return context.builder.app_flow_validate(
1327
+ profile=args.profile,
1328
+ spec=payload,
1329
+ schema_version=args.schema_version or None,
1330
+ )
1331
+
1332
+
1331
1333
  def _handle_flow_get(args: argparse.Namespace, context: CliContext) -> dict:
1332
1334
  return context.builder.app_flow_get(profile=args.profile, app_key=args.app_key, version_id=args.version_id)
1333
1335
 
1334
1336
 
1337
+ def _handle_flow_versions(args: argparse.Namespace, context: CliContext) -> dict:
1338
+ return context.builder.app_flow_list_versions(profile=args.profile, app_key=args.app_key)
1339
+
1340
+
1341
+ def _handle_flow_restore(args: argparse.Namespace, context: CliContext) -> dict:
1342
+ return context.builder.app_flow_restore_version(
1343
+ profile=args.profile,
1344
+ app_key=args.app_key,
1345
+ version_id=args.version_id,
1346
+ idempotency_key=args.idempotency_key or None,
1347
+ schema_version=args.schema_version or None,
1348
+ )
1349
+
1350
+
1335
1351
  def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
1336
1352
  apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
1337
1353
  if apps is None and not (args.app_key or "").strip():
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import textwrap
5
+
6
+ from ..context import CliContext
7
+ from .common import load_list_arg, require_list_arg, require_object_arg, raise_config_error
8
+
9
+
10
+ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
11
+ parser = subparsers.add_parser(
12
+ "data",
13
+ help="默认应用数据查询与直接增删改",
14
+ formatter_class=argparse.RawDescriptionHelpFormatter,
15
+ epilog=textwrap.dedent("""
16
+ JSON 文件约定:
17
+ columns.json [101, 102]
18
+ where.json [{"field_id": 101, "op": "eq", "value": "A"}]
19
+ sort.json [{"field_id": 101, "direction": "asc"}]
20
+ items.json [{"fields": {"101": "A"}}]
21
+ fields.json {"101": "B"}
22
+ query/get/insert/update 默认返回 field_capabilities;可用 --no-schema 关闭。
23
+ """).strip(),
24
+ )
25
+ commands = parser.add_subparsers(dest="data_command", required=True, metavar="{query,get,insert,update,delete}")
26
+
27
+ query = commands.add_parser(
28
+ "query", help="分页查询应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
29
+ epilog='最小示例:qingflow data query --app-key APP_1 --where-file where.json\n'
30
+ 'where.json: [{"field_id": 101, "op": "eq", "value": "A"}]',
31
+ )
32
+ query.add_argument("--app-key", required=True)
33
+ query.add_argument("--columns-file", help='JSON 数组,例如 [101, 102]')
34
+ query.add_argument(
35
+ "--where-file",
36
+ help='JSON 数组;项为 {"field_id":101,"op":"eq","value":"A"},in/not_in 的 value 必须为数组',
37
+ )
38
+ query.add_argument("--sort-file", help='JSON 数组,例如 [{"field_id":101,"direction":"asc"}]')
39
+ query.add_argument("--search", default="")
40
+ query.add_argument("--page", type=int, default=1)
41
+ query.add_argument("--page-size", type=int, default=100)
42
+ query.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
43
+ query.add_argument("--all", action="store_true", dest="all_pages")
44
+ query.add_argument("--output", choices=("json", "csv"), default="json")
45
+ query.set_defaults(handler=_handle_query, format_hint="data_query")
46
+
47
+ get = commands.add_parser("get", help="读取单条应用数据")
48
+ get.add_argument("--app-key", required=True)
49
+ get.add_argument("--record-id", required=True)
50
+ get.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
51
+ get.set_defaults(handler=_handle_get, format_hint="data_get")
52
+
53
+ insert = commands.add_parser(
54
+ "insert", help="新增应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
55
+ epilog='最小示例:items.json = [{"fields": {"101": "A"}}]',
56
+ )
57
+ insert.add_argument("--app-key", required=True)
58
+ insert.add_argument("--items-file", required=True, help='JSON 数组,项为 {"fields":{"字段ID":值}}')
59
+ insert.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
60
+ insert.set_defaults(handler=_handle_insert, format_hint="data_insert")
61
+
62
+ update = commands.add_parser(
63
+ "update", help="更新应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
64
+ epilog='最小示例:fields.json = {"101": "B"}',
65
+ )
66
+ update.add_argument("--app-key", required=True)
67
+ update.add_argument("--record-id", required=True)
68
+ update.add_argument("--fields-file", required=True, help='JSON 对象,键为字符串形式 field_id')
69
+ update.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
70
+ update.set_defaults(handler=_handle_update, format_hint="data_update")
71
+
72
+ delete = commands.add_parser("delete", help="删除应用数据")
73
+ delete.add_argument("--app-key", required=True)
74
+ delete.add_argument(
75
+ "--record-ids", action="append", default=[], metavar="ID[,ID...]",
76
+ help="待删除记录 ID;支持逗号分隔,也可重复传入",
77
+ )
78
+ delete.add_argument("--record-ids-file", help="附加的记录 ID JSON 数组文件")
79
+ delete.set_defaults(handler=_handle_delete, format_hint="data_delete")
80
+
81
+
82
+ def _handle_query(args: argparse.Namespace, context: CliContext) -> dict:
83
+ if args.all_pages != (args.output == "csv"):
84
+ raise_config_error(
85
+ "--all 与 --output csv 必须同时使用。",
86
+ fix_hint="使用 `qingflow data query ... --all --output csv`,普通分页查询不要传这两个参数。",
87
+ )
88
+ return context.data.data_query(
89
+ profile=args.profile,
90
+ app_key=args.app_key,
91
+ columns=load_list_arg(args.columns_file, option_name="--columns-file"),
92
+ where=load_list_arg(args.where_file, option_name="--where-file"),
93
+ sort=load_list_arg(args.sort_file, option_name="--sort-file"),
94
+ search=args.search,
95
+ page=args.page,
96
+ page_size=args.page_size,
97
+ include_schema=args.include_schema,
98
+ all_pages=args.all_pages,
99
+ output=args.output,
100
+ )
101
+
102
+
103
+ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
104
+ return context.data.data_get(
105
+ profile=args.profile, app_key=args.app_key, record_id=args.record_id, include_schema=args.include_schema
106
+ )
107
+
108
+
109
+ def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
110
+ return context.data.data_insert(
111
+ profile=args.profile, app_key=args.app_key,
112
+ items=require_list_arg(args.items_file, option_name="--items-file"), include_schema=args.include_schema,
113
+ )
114
+
115
+
116
+ def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
117
+ return context.data.data_update(
118
+ profile=args.profile, app_key=args.app_key, record_id=args.record_id,
119
+ fields=require_object_arg(args.fields_file, option_name="--fields-file"), include_schema=args.include_schema,
120
+ )
121
+
122
+
123
+ def _handle_delete(args: argparse.Namespace, context: CliContext) -> dict:
124
+ record_ids = [item.strip() for group in args.record_ids for item in group.split(",") if item.strip()]
125
+ record_ids.extend(load_list_arg(args.record_ids_file, option_name="--record-ids-file"))
126
+ if not record_ids:
127
+ raise_config_error(
128
+ "至少传入一个记录 ID。",
129
+ fix_hint="使用 `--record-ids ID[,ID...]`;也可附加 `--record-ids-file ids.json`。",
130
+ )
131
+ return context.data.data_delete(
132
+ profile=args.profile, app_key=args.app_key,
133
+ record_ids=record_ids,
134
+ )
@@ -9,7 +9,7 @@ from .common import load_list_arg, load_object_arg, raise_config_error, require_
9
9
 
10
10
 
11
11
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
12
- parser = subparsers.add_parser("record", help="记录与表结构")
12
+ parser = subparsers.add_parser("record", help="日志、附件、导入、代码块及 record 专项能力")
13
13
  record_subparsers = parser.add_subparsers(
14
14
  dest="record_command",
15
15
  required=True,
@@ -10,12 +10,13 @@ from ..session_store import SessionStore
10
10
  from ..tools.ai_builder_tools import AiBuilderTools
11
11
  from ..tools.app_tools import AppTools
12
12
  from ..tools.auth_tools import AuthTools
13
- from ..tools.login_link_tools import LoginLinkTools
14
13
  from ..tools.code_block_tools import CodeBlockTools
14
+ from ..tools.data_tools import DataTools
15
15
  from ..tools.export_tools import ExportTools
16
16
  from ..tools.feedback_tools import FeedbackTools
17
17
  from ..tools.file_tools import FileTools
18
18
  from ..tools.import_tools import ImportTools
19
+ from ..tools.login_link_tools import LoginLinkTools
19
20
  from ..tools.record_tools import RecordTools
20
21
  from ..tools.resource_read_tools import ResourceReadTools
21
22
  from ..tools.task_context_tools import TaskContextTools
@@ -33,6 +34,7 @@ class CliContext:
33
34
  app: AppTools
34
35
  resource: ResourceReadTools
35
36
  record: RecordTools
37
+ data: DataTools
36
38
  code_block: CodeBlockTools
37
39
  imports: ImportTools
38
40
  exports: ExportTools
@@ -64,6 +66,7 @@ def build_cli_context() -> CliContext:
64
66
  app=AppTools(sessions, backend),
65
67
  resource=ResourceReadTools(sessions, backend),
66
68
  record=RecordTools(sessions, backend),
69
+ data=DataTools(sessions, backend),
67
70
  code_block=CodeBlockTools(sessions, backend),
68
71
  imports=ImportTools(sessions, backend),
69
72
  exports=ExportTools(sessions, backend),