@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: qingflow-common
|
|
3
|
-
description:
|
|
3
|
+
description: Explain shared Qingflow concepts, choose the CLI or MCP surface, and route resource work or page-link delivery to the relevant domain Skill.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Qingflow Common Concepts
|
|
@@ -19,7 +19,8 @@ Do not require both surfaces. If only one is installed, complete the workflow on
|
|
|
19
19
|
## Shared Concepts
|
|
20
20
|
|
|
21
21
|
- **AppForm** is the versioned declarative definition for an application form: app metadata, fields, sections, layout, settings, and field configuration. The same JSON declaration is used by CLI and Qingflow MCP.
|
|
22
|
-
- **
|
|
22
|
+
- **Data** 是既有应用中的普通业务数据。查询、单条读取和直接增删改默认使用视图无关的 data 入口,并以当次 schema、字段能力和访问范围为准。
|
|
23
|
+
- **Record** 用于详情页附件/图片、完整日志、导入、代码块,以及用户明确指定的 record 命令;不要把普通应用数据工作路由到 record。
|
|
23
24
|
- **Task** is a workflow work item. Prefer task operations for approval, transfer, rollback, and assignee actions.
|
|
24
25
|
- **Builder resource** is package, app, view, chart, portal, navigation, or workflow configuration. It is separate from record data.
|
|
25
26
|
- **Schema version** is part of the public contract. Pin it for one AppForm or workflow operation and do not combine values from different versions.
|
|
@@ -30,11 +31,13 @@ Do not require both surfaces. If only one is installed, complete the workflow on
|
|
|
30
31
|
| --- | --- | --- |
|
|
31
32
|
| Form schema/read/apply/delete | `qingflow --json builder app-form ...` / `builder app delete` | `app_form_schema`, `app_form_get`, `app_form_apply`, `app_delete` |
|
|
32
33
|
| AppForm static validation | `qingflow --json builder app-form validate --file ...` | Performed inside `app_form_apply`; no separate tool |
|
|
33
|
-
|
|
|
34
|
+
| 普通应用数据查询与直接增删改 | `qingflow --json data ...` | Qingflow MCP `data_query/get/insert/update/delete` |
|
|
35
|
+
| 日志、附件、导入、代码块或用户指定的 record 命令 | `qingflow --json record ...` / `import ...` | Qingflow MCP 对应 record/import 专项工具 |
|
|
36
|
+
| 待办与流程动作 | `qingflow --json task ...` | Qingflow MCP task tools |
|
|
34
37
|
| 免登录打开网页或指定业务页面 | `qingflow --json auth login-link [--redirect-path PATH]` | `get_login_link` |
|
|
35
38
|
| Package, view, portal, chart, workflow | `qingflow --json builder ...` | Qingflow MCP configuration tools |
|
|
36
39
|
|
|
37
|
-
The operation names and payload schemas are defined by the public surface. Use the current Schema or tool input schema as the source of truth, not
|
|
40
|
+
The operation names and payload schemas are defined by the public surface. Use the current Schema or tool input schema as the source of truth, not another command's shape or a backend payload.
|
|
38
41
|
|
|
39
42
|
## Package Boundaries
|
|
40
43
|
|
|
@@ -43,20 +46,10 @@ The operation names and payload schemas are defined by the public surface. Use t
|
|
|
43
46
|
|
|
44
47
|
Each package includes this common Skill. The CLI package installs its CLI guidance; the MCP package installs every MCP domain Skill. The source of every packaged Skill is `qingflow-support/mcp-server/skills/`.
|
|
45
48
|
|
|
46
|
-
##
|
|
49
|
+
## Deliver Qingflow Page Links
|
|
47
50
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
- 目标路径使用用户提供或业务工具确认的站内路径,不根据应用、记录标识猜测地址。
|
|
51
|
-
- 用户明确指定页面但路径未知时继续定位或说明限制,不猜测,也不静默替换成首页。搜索未命中时继续搜索 `login` 或查看服务工具列表,不擅自打开固定正式站点;失败时如实说明。
|
|
52
|
-
|
|
53
|
-
- 默认使用当前会话及已选工作区;需要指定会话时使用 MCP 的 `profile` 或 CLI 的 `--profile`。
|
|
54
|
-
- `redirect_path` 可选,默认空:省略时进入工作区首页;传入时仅支持以单个 `/` 开头的站内业务路径,可带 query/hash。不得传完整 URL、外部地址、认证入口或多层编码路径。
|
|
55
|
-
- 示例:`get_login_link({})`;指定目标示例:`get_login_link({"redirect_path":"/index/app/123?view=abc"})`。示例标识仅作格式展示,实际目标必须来自用户提供或已经确认的页面信息,不得编造应用、记录标识。
|
|
56
|
-
- 临近交付时再生成,使用返回的 `url`,文案贴合“查看记录”“继续编辑”等上下文,不展示工作区数字 ID 或原始 JSON。
|
|
57
|
-
- CLI 示例:`qingflow --json auth login-link`;指定目标使用 `qingflow --profile default --json auth login-link --redirect-path "/index/app/123?view=abc"`,同样不得编造实际路径。
|
|
58
|
-
- 链接短时有效且仅可使用一次,失效后重新获取,不修改或拼造链接;只交付给请求用户,不写入公共文档或日志。
|
|
59
|
-
- 用户要求“给我链接”时仅交付,不提前消费;要求“打开”时才使用可用浏览器打开。浏览器不可用时提供可点击链接,不假称已经打开。
|
|
51
|
+
- After creating, changing, or installing resources, finish with the result and a clickable entry for each requested primary result. Use confirmed stable `uri` values, deduplicate them, and link only confirmed successes. Do not auto-issue links for data-only answers, intermediate reads, failed or deleted resources, or when the user declines links.
|
|
52
|
+
- When browser login is unknown, issue one fresh link per displayed destination near delivery through the installed surface. Never guess, replace, open, or consume a destination unless authorized. If issuance fails, keep the business result and offer the stable URI as requiring login when available. Follow the CLI Skill or MCP tool description for signing details.
|
|
60
53
|
|
|
61
54
|
## Route Next
|
|
62
55
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
import os
|
|
2
3
|
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
from threading import Event
|
|
@@ -9,7 +10,14 @@ from uuid import uuid4
|
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
|
|
12
|
-
from .config import
|
|
13
|
+
from .config import (
|
|
14
|
+
DEFAULT_USER_AGENT,
|
|
15
|
+
get_default_qf_version,
|
|
16
|
+
get_http_retry_attempts,
|
|
17
|
+
get_http_retry_base_seconds,
|
|
18
|
+
get_timeout_seconds,
|
|
19
|
+
normalize_base_url,
|
|
20
|
+
)
|
|
13
21
|
from .errors import QingflowApiError
|
|
14
22
|
from .json_types import JSONObject, JSONScalar, JSONValue
|
|
15
23
|
|
|
@@ -65,6 +73,7 @@ class BackendClient:
|
|
|
65
73
|
params: JSONObject | None = None,
|
|
66
74
|
json_body: JSONValue = None,
|
|
67
75
|
unwrap: bool = True,
|
|
76
|
+
unwrap_inner_envelope: bool = True,
|
|
68
77
|
qf_version: str | None = None,
|
|
69
78
|
) -> JSONValue:
|
|
70
79
|
return self.public_request_with_meta(
|
|
@@ -74,6 +83,7 @@ class BackendClient:
|
|
|
74
83
|
params=params,
|
|
75
84
|
json_body=json_body,
|
|
76
85
|
unwrap=unwrap,
|
|
86
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
77
87
|
qf_version=qf_version,
|
|
78
88
|
).data
|
|
79
89
|
|
|
@@ -86,6 +96,7 @@ class BackendClient:
|
|
|
86
96
|
params: JSONObject | None = None,
|
|
87
97
|
json_body: JSONValue = None,
|
|
88
98
|
unwrap: bool = True,
|
|
99
|
+
unwrap_inner_envelope: bool = True,
|
|
89
100
|
qf_version: str | None = None,
|
|
90
101
|
) -> BackendResponse:
|
|
91
102
|
return self._request_with_meta(
|
|
@@ -95,6 +106,7 @@ class BackendClient:
|
|
|
95
106
|
json_body=json_body,
|
|
96
107
|
headers=self._base_headers(None, None, qf_version=qf_version),
|
|
97
108
|
unwrap=unwrap,
|
|
109
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
98
110
|
)
|
|
99
111
|
|
|
100
112
|
def public_request_with_headers(
|
|
@@ -106,6 +118,7 @@ class BackendClient:
|
|
|
106
118
|
params: JSONObject | None = None,
|
|
107
119
|
json_body: JSONValue = None,
|
|
108
120
|
unwrap: bool = True,
|
|
121
|
+
unwrap_inner_envelope: bool = True,
|
|
109
122
|
qf_version: str | None = None,
|
|
110
123
|
headers: dict[str, str] | None = None,
|
|
111
124
|
) -> BackendResponse:
|
|
@@ -119,6 +132,7 @@ class BackendClient:
|
|
|
119
132
|
json_body=json_body,
|
|
120
133
|
headers=request_headers,
|
|
121
134
|
unwrap=unwrap,
|
|
135
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
122
136
|
)
|
|
123
137
|
|
|
124
138
|
def request(
|
|
@@ -130,6 +144,7 @@ class BackendClient:
|
|
|
130
144
|
params: JSONObject | None = None,
|
|
131
145
|
json_body: JSONValue = None,
|
|
132
146
|
unwrap: bool = True,
|
|
147
|
+
unwrap_inner_envelope: bool = True,
|
|
133
148
|
) -> JSONValue:
|
|
134
149
|
return self.request_with_meta(
|
|
135
150
|
method,
|
|
@@ -138,6 +153,7 @@ class BackendClient:
|
|
|
138
153
|
params=params,
|
|
139
154
|
json_body=json_body,
|
|
140
155
|
unwrap=unwrap,
|
|
156
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
141
157
|
).data
|
|
142
158
|
|
|
143
159
|
def request_with_meta(
|
|
@@ -149,6 +165,7 @@ class BackendClient:
|
|
|
149
165
|
params: JSONObject | None = None,
|
|
150
166
|
json_body: JSONValue = None,
|
|
151
167
|
unwrap: bool = True,
|
|
168
|
+
unwrap_inner_envelope: bool = True,
|
|
152
169
|
) -> BackendResponse:
|
|
153
170
|
return self._request_with_meta(
|
|
154
171
|
method,
|
|
@@ -162,6 +179,7 @@ class BackendClient:
|
|
|
162
179
|
qf_version=context.qf_version,
|
|
163
180
|
),
|
|
164
181
|
unwrap=unwrap,
|
|
182
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
165
183
|
)
|
|
166
184
|
|
|
167
185
|
def stream_request(
|
|
@@ -353,6 +371,7 @@ class BackendClient:
|
|
|
353
371
|
data: dict[str, Any] | None = None,
|
|
354
372
|
files: dict[str, tuple[str, bytes, str | None]] | None = None,
|
|
355
373
|
unwrap: bool = True,
|
|
374
|
+
unwrap_inner_envelope: bool = True,
|
|
356
375
|
) -> JSONValue:
|
|
357
376
|
return self.request_multipart_with_meta(
|
|
358
377
|
method,
|
|
@@ -361,6 +380,7 @@ class BackendClient:
|
|
|
361
380
|
data=data,
|
|
362
381
|
files=files,
|
|
363
382
|
unwrap=unwrap,
|
|
383
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
364
384
|
).data
|
|
365
385
|
|
|
366
386
|
def request_multipart_with_meta(
|
|
@@ -372,6 +392,7 @@ class BackendClient:
|
|
|
372
392
|
data: dict[str, Any] | None = None,
|
|
373
393
|
files: dict[str, tuple[str, bytes, str | None]] | None = None,
|
|
374
394
|
unwrap: bool = True,
|
|
395
|
+
unwrap_inner_envelope: bool = True,
|
|
375
396
|
) -> BackendResponse:
|
|
376
397
|
headers = self._base_headers(
|
|
377
398
|
context.token,
|
|
@@ -392,7 +413,12 @@ class BackendClient:
|
|
|
392
413
|
)
|
|
393
414
|
except httpx.RequestError as exc:
|
|
394
415
|
raise QingflowApiError(category="network", message=str(exc), request_id=headers["Qf-Request-Id"])
|
|
395
|
-
parsed = self._parse_response(
|
|
416
|
+
parsed = self._parse_response(
|
|
417
|
+
response,
|
|
418
|
+
headers["Qf-Request-Id"],
|
|
419
|
+
unwrap=unwrap,
|
|
420
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
421
|
+
)
|
|
396
422
|
return BackendResponse(
|
|
397
423
|
data=parsed,
|
|
398
424
|
headers=dict(response.headers),
|
|
@@ -646,13 +672,26 @@ class BackendClient:
|
|
|
646
672
|
json_body: JSONValue,
|
|
647
673
|
headers: dict[str, str],
|
|
648
674
|
unwrap: bool,
|
|
675
|
+
unwrap_inner_envelope: bool = True,
|
|
649
676
|
) -> BackendResponse:
|
|
650
|
-
|
|
677
|
+
# 评测沉淀的传输层重试回流(eval_replay_flow._http_json 同款):
|
|
678
|
+
# 3 次尝试 + 线性退避(base × attempt,即 3s/6s),env 可配。
|
|
679
|
+
# GET/HEAD 任何网络错误可重试;写方法(POST/PUT/PATCH/DELETE)仅
|
|
680
|
+
# ConnectError/ConnectTimeout(请求确证未发出)可重试,避免双重写。
|
|
681
|
+
# HTTP 状态码/业务码错误由 _parse_response 抛出,不属网络错误,不重试。
|
|
682
|
+
normalized_method = method.upper()
|
|
683
|
+
attempts = get_http_retry_attempts()
|
|
684
|
+
retry_base_seconds = get_http_retry_base_seconds()
|
|
651
685
|
last_error: QingflowApiError | None = None
|
|
652
|
-
for
|
|
686
|
+
for attempt in range(1, attempts + 1):
|
|
653
687
|
try:
|
|
654
|
-
response = self._client.request(
|
|
655
|
-
parsed = self._parse_response(
|
|
688
|
+
response = self._client.request(normalized_method, url, params=params, json=json_body, headers=headers)
|
|
689
|
+
parsed = self._parse_response(
|
|
690
|
+
response,
|
|
691
|
+
headers["Qf-Request-Id"],
|
|
692
|
+
unwrap=unwrap,
|
|
693
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
694
|
+
)
|
|
656
695
|
return BackendResponse(
|
|
657
696
|
data=parsed,
|
|
658
697
|
headers=dict(response.headers),
|
|
@@ -661,7 +700,18 @@ class BackendClient:
|
|
|
661
700
|
qf_response_version=self._extract_response_qf_version(response.headers),
|
|
662
701
|
)
|
|
663
702
|
except httpx.RequestError as exc:
|
|
664
|
-
|
|
703
|
+
retryable = normalized_method in {"GET", "HEAD"} or isinstance(
|
|
704
|
+
exc, (httpx.ConnectError, httpx.ConnectTimeout)
|
|
705
|
+
)
|
|
706
|
+
last_error = QingflowApiError(
|
|
707
|
+
category="network",
|
|
708
|
+
message=str(exc),
|
|
709
|
+
request_id=headers["Qf-Request-Id"],
|
|
710
|
+
details={"retry_attempts": attempt},
|
|
711
|
+
)
|
|
712
|
+
if not retryable or attempt >= attempts:
|
|
713
|
+
break
|
|
714
|
+
sleep(retry_base_seconds * attempt)
|
|
665
715
|
assert last_error is not None
|
|
666
716
|
raise last_error
|
|
667
717
|
|
|
@@ -695,7 +745,14 @@ class BackendClient:
|
|
|
695
745
|
except httpx.RequestError as exc:
|
|
696
746
|
raise QingflowApiError(category="network", message=str(exc), request_id=headers["Qf-Request-Id"])
|
|
697
747
|
|
|
698
|
-
def _parse_response(
|
|
748
|
+
def _parse_response(
|
|
749
|
+
self,
|
|
750
|
+
response: httpx.Response,
|
|
751
|
+
request_id: str,
|
|
752
|
+
*,
|
|
753
|
+
unwrap: bool,
|
|
754
|
+
unwrap_inner_envelope: bool = True,
|
|
755
|
+
) -> JSONValue:
|
|
699
756
|
payload: JSONValue
|
|
700
757
|
try:
|
|
701
758
|
payload = response.json()
|
|
@@ -708,12 +765,25 @@ class BackendClient:
|
|
|
708
765
|
backend_code=self._extract_code(payload),
|
|
709
766
|
request_id=request_id,
|
|
710
767
|
http_status=response.status_code,
|
|
768
|
+
details=self._extract_error_details(payload),
|
|
711
769
|
)
|
|
712
770
|
if not unwrap:
|
|
713
771
|
return payload
|
|
714
|
-
return self._unwrap_payload(
|
|
772
|
+
return self._unwrap_payload(
|
|
773
|
+
payload,
|
|
774
|
+
request_id,
|
|
775
|
+
response.status_code,
|
|
776
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
777
|
+
)
|
|
715
778
|
|
|
716
|
-
def _unwrap_payload(
|
|
779
|
+
def _unwrap_payload(
|
|
780
|
+
self,
|
|
781
|
+
payload: JSONValue,
|
|
782
|
+
request_id: str,
|
|
783
|
+
http_status: int,
|
|
784
|
+
*,
|
|
785
|
+
unwrap_inner_envelope: bool = True,
|
|
786
|
+
) -> JSONValue:
|
|
717
787
|
if not isinstance(payload, dict):
|
|
718
788
|
return payload
|
|
719
789
|
if "success" in payload:
|
|
@@ -725,7 +795,12 @@ class BackendClient:
|
|
|
725
795
|
request_id=request_id,
|
|
726
796
|
http_status=http_status,
|
|
727
797
|
)
|
|
728
|
-
return self.
|
|
798
|
+
return self._guard_inner_envelope(
|
|
799
|
+
self._extract_success_data(payload),
|
|
800
|
+
request_id,
|
|
801
|
+
http_status,
|
|
802
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
803
|
+
)
|
|
729
804
|
code = self._extract_code(payload)
|
|
730
805
|
if code not in (None, 0, "0"):
|
|
731
806
|
raise QingflowApiError(
|
|
@@ -736,9 +811,44 @@ class BackendClient:
|
|
|
736
811
|
http_status=http_status,
|
|
737
812
|
)
|
|
738
813
|
if code in (0, "0"):
|
|
739
|
-
return self.
|
|
814
|
+
return self._guard_inner_envelope(
|
|
815
|
+
self._extract_success_data(payload),
|
|
816
|
+
request_id,
|
|
817
|
+
http_status,
|
|
818
|
+
unwrap_inner_envelope=unwrap_inner_envelope,
|
|
819
|
+
)
|
|
740
820
|
return payload
|
|
741
821
|
|
|
822
|
+
def _guard_inner_envelope(
|
|
823
|
+
self,
|
|
824
|
+
extracted: JSONValue,
|
|
825
|
+
request_id: str,
|
|
826
|
+
http_status: int,
|
|
827
|
+
*,
|
|
828
|
+
unwrap_inner_envelope: bool,
|
|
829
|
+
) -> JSONValue:
|
|
830
|
+
"""双层信封防御:后端错误经 ResponseResult.failure(code, message, body) 转为
|
|
831
|
+
``HTTP 200 + 外层 code=0 + data={code, message}`` 形态(内层错误对象被塞进 data)。
|
|
832
|
+
|
|
833
|
+
外层信封检查通过后,若提取出的业务 payload 本身携带错误 code,则视为业务失败
|
|
834
|
+
抛 QingflowApiError,而不是把错误对象当“业务数据”上抛(评测 r2 实测 219 个读面
|
|
835
|
+
gap 全为此形态)。逐调用方可传 ``unwrap_inner_envelope=False`` 豁免(当某 API 的
|
|
836
|
+
data.code 是合法业务字段时)。
|
|
837
|
+
"""
|
|
838
|
+
if not unwrap_inner_envelope or not isinstance(extracted, dict):
|
|
839
|
+
return extracted
|
|
840
|
+
inner_code = self._extract_code(extracted)
|
|
841
|
+
if inner_code in (None, 0, "0"):
|
|
842
|
+
return extracted
|
|
843
|
+
raise QingflowApiError(
|
|
844
|
+
category="backend",
|
|
845
|
+
message=self._extract_message(extracted) or "Qingflow request failed (inner envelope)",
|
|
846
|
+
backend_code=inner_code,
|
|
847
|
+
request_id=request_id,
|
|
848
|
+
http_status=http_status,
|
|
849
|
+
details={"envelope": "inner"},
|
|
850
|
+
)
|
|
851
|
+
|
|
742
852
|
def _extract_success_data(self, payload: JSONObject) -> JSONValue:
|
|
743
853
|
for key in ("result", "data", "page", "obj"):
|
|
744
854
|
if key in payload:
|
|
@@ -759,6 +869,23 @@ class BackendClient:
|
|
|
759
869
|
return payload.get("code", payload.get("errCode"))
|
|
760
870
|
return None
|
|
761
871
|
|
|
872
|
+
def _extract_error_details(self, payload: JSONValue) -> JSONObject | None:
|
|
873
|
+
"""Best-effort extraction of the backend structured error body.
|
|
874
|
+
|
|
875
|
+
Qingflow ``ResponseResult.failure(code, message, data)`` serializes the structured error
|
|
876
|
+
into ``data`` (older/other endpoints may use ``result``), e.g. workflow-spec validation
|
|
877
|
+
bodies with ``code`` / ``path`` / ``suggestion`` and negotiation payloads like
|
|
878
|
+
``currentSpec``. Keeping it on the error lets facades surface backend negotiation
|
|
879
|
+
contexts instead of swallowing them.
|
|
880
|
+
"""
|
|
881
|
+
if not isinstance(payload, dict):
|
|
882
|
+
return None
|
|
883
|
+
for key in ("result", "data"):
|
|
884
|
+
body = payload.get(key)
|
|
885
|
+
if isinstance(body, dict):
|
|
886
|
+
return dict(body)
|
|
887
|
+
return None
|
|
888
|
+
|
|
762
889
|
def _base_headers(
|
|
763
890
|
self,
|
|
764
891
|
token: str | None,
|
|
@@ -778,6 +905,9 @@ class BackendClient:
|
|
|
778
905
|
headers["token"] = token
|
|
779
906
|
if ws_id is not None:
|
|
780
907
|
headers["wsId"] = str(ws_id)
|
|
908
|
+
# #147/r8: benchmark replay mode flag——backend skips authored topology overrides
|
|
909
|
+
if os.environ.get("EVAL_PRESERVE_TOPOLOGY", "").lower() in ("1", "true", "yes"):
|
|
910
|
+
headers["X-Preserve-Topology"] = "true"
|
|
781
911
|
return headers
|
|
782
912
|
|
|
783
913
|
def _resolve_qf_version(self, explicit_qf_version: str | None) -> tuple[str | None, str]:
|