@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.
- 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 +92 -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/backend_client.py +142 -12
- package/src/qingflow_mcp/builder_facade/flow_validation.py +569 -0
- package/src/qingflow_mcp/builder_facade/service.py +749 -6
- package/src/qingflow_mcp/builder_facade/workflow_spec.py +154 -2
- 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/context.py +4 -1
- package/src/qingflow_mcp/cli/formatters.py +159 -4
- package/src/qingflow_mcp/cli/main.py +4 -1
- 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 +53 -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 +11 -46
- 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
|
-
|
|
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 =
|
|
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
|
-
#
|
|
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),
|