@qingflow-tech/qingflow-cli 1.1.61-beta.4 → 1.1.61
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 +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -2
- package/skills/qingflow-cli/SKILL.md +2 -104
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +2 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +5 -9
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
- package/src/qingflow_mcp/app_form_compat.py +0 -3
- package/src/qingflow_mcp/app_form_validator.py +17 -69
- package/src/qingflow_mcp/builder_facade/app_form.py +100 -497
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +0 -1
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +30 -64
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +18 -11
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +10 -6
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +15 -46
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +27 -61
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +8 -4
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +0 -43
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +12 -7
- package/src/qingflow_mcp/builder_facade/service.py +1 -39
- package/src/qingflow_mcp/cli/main.py +1 -34
- package/src/qingflow_mcp/cloud_context.py +1 -16
- package/src/qingflow_mcp/public_surface.py +8 -67
- package/src/qingflow_mcp/response_trim.py +1 -2
- package/src/qingflow_mcp/server.py +6 -12
- package/src/qingflow_mcp/server_app_builder.py +5 -9
- package/src/qingflow_mcp/server_app_user.py +5 -9
- package/src/qingflow_mcp/server_cloud.py +7 -43
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +0 -146
- package/src/qingflow_mcp/tool_catalog.py +0 -387
- package/src/qingflow_mcp/tool_catalog_i18n.py +0 -185
- package/src/qingflow_mcp/tool_policy.py +0 -197
|
@@ -136,10 +136,10 @@
|
|
|
136
136
|
]
|
|
137
137
|
}
|
|
138
138
|
],
|
|
139
|
-
"description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name
|
|
139
|
+
"description": "字段选择器;用于定位应用的数据标题、数据封面、自定义编号取值来源或其他配置引用的字段。至少提供 queId 或 name,两者同时提供时必须指向同一字段。",
|
|
140
140
|
"properties": {
|
|
141
141
|
"name": {
|
|
142
|
-
"description": "
|
|
142
|
+
"description": "字段名称;仅在当前作用域内唯一时可用于解析。",
|
|
143
143
|
"minLength": 1,
|
|
144
144
|
"type": "string"
|
|
145
145
|
},
|
|
@@ -258,9 +258,13 @@
|
|
|
258
258
|
"type": "array"
|
|
259
259
|
},
|
|
260
260
|
"width": {
|
|
261
|
-
"description": "
|
|
262
|
-
"
|
|
263
|
-
|
|
261
|
+
"description": "字段在所在行的宽度百分比;只能选择 25、50、75 或 100。",
|
|
262
|
+
"enum": [
|
|
263
|
+
25,
|
|
264
|
+
50,
|
|
265
|
+
75,
|
|
266
|
+
100
|
|
267
|
+
],
|
|
264
268
|
"type": "integer"
|
|
265
269
|
}
|
|
266
270
|
},
|
|
@@ -330,10 +334,11 @@
|
|
|
330
334
|
"description": "段落判别值。"
|
|
331
335
|
},
|
|
332
336
|
"rows": {
|
|
333
|
-
"description": "
|
|
337
|
+
"description": "段落内的非空有序字段行。",
|
|
334
338
|
"items": {
|
|
335
339
|
"$ref": "#/$defs/FieldRow"
|
|
336
340
|
},
|
|
341
|
+
"minItems": 1,
|
|
337
342
|
"type": "array"
|
|
338
343
|
},
|
|
339
344
|
"sectionId": {
|
|
@@ -378,6 +383,7 @@
|
|
|
378
383
|
],
|
|
379
384
|
"type": "object"
|
|
380
385
|
},
|
|
386
|
+
"minItems": 1,
|
|
381
387
|
"type": "array"
|
|
382
388
|
},
|
|
383
389
|
"sectionId": {
|
|
@@ -689,7 +695,6 @@
|
|
|
689
695
|
"x-qingflow-supported-field-types": [
|
|
690
696
|
"text",
|
|
691
697
|
"long_text",
|
|
692
|
-
"rich_text",
|
|
693
698
|
"number",
|
|
694
699
|
"amount",
|
|
695
700
|
"link",
|
|
@@ -163,14 +163,9 @@ FIELD_TYPE_TO_QUESTION_TYPE: dict[str, int] = {
|
|
|
163
163
|
FieldType.relation.value: 25,
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
# code_block / q_linker 输出目标字段类型。后端写链无类型白名单(FormSvcImpl 原样
|
|
167
|
-
# 落库),运行时 DataSvcImpl.getAnswerValueByString:attachment 按拆分 URL 写入、
|
|
168
|
-
# subtable 整表组装(均有专门分支),rich_text/data_relation 走 Question.canImport
|
|
169
|
-
# 兜底按字符串写入(不报错)——四者为存量合法配置,与 validator 侧白名单同口径。
|
|
170
166
|
INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
171
167
|
FieldType.text.value,
|
|
172
168
|
FieldType.long_text.value,
|
|
173
|
-
"rich_text",
|
|
174
169
|
FieldType.number.value,
|
|
175
170
|
FieldType.amount.value,
|
|
176
171
|
FieldType.date.value,
|
|
@@ -178,9 +173,6 @@ INTEGRATION_OUTPUT_TARGET_FIELD_TYPES: tuple[str, ...] = (
|
|
|
178
173
|
FieldType.single_select.value,
|
|
179
174
|
FieldType.multi_select.value,
|
|
180
175
|
FieldType.boolean.value,
|
|
181
|
-
FieldType.attachment.value,
|
|
182
|
-
"data_relation",
|
|
183
|
-
FieldType.subtable.value,
|
|
184
176
|
)
|
|
185
177
|
|
|
186
178
|
MATCH_TYPE_ACCURACY = 1
|
|
@@ -23245,23 +23237,6 @@ def _apply_field_mutation(field: dict[str, Any], mutation: Any) -> None:
|
|
|
23245
23237
|
field.pop("_question_rebuild_required", None)
|
|
23246
23238
|
|
|
23247
23239
|
|
|
23248
|
-
class AmbiguousFieldSelector(ValueError):
|
|
23249
|
-
"""name-only selector 命中多个同名字段。
|
|
23250
|
-
|
|
23251
|
-
方案 A(duplicate-field-name):后端以 queId 为字段身份、允许同父级同名;
|
|
23252
|
-
name 命中多个字段时禁止静默取第一个,必须显式失败并给出候选 queId。
|
|
23253
|
-
compiler 包装层(app_form)把它转写为 AMBIGUOUS_FIELD_SELECTOR 错误码。
|
|
23254
|
-
"""
|
|
23255
|
-
|
|
23256
|
-
def __init__(self, location: str, candidates: list[int]) -> None:
|
|
23257
|
-
self.location = location
|
|
23258
|
-
self.candidates = candidates
|
|
23259
|
-
super().__init__(
|
|
23260
|
-
f"{location} matches multiple fields with the same name; "
|
|
23261
|
-
f"use queId to disambiguate (candidate queIds: {sorted(candidates)})"
|
|
23262
|
-
)
|
|
23263
|
-
|
|
23264
|
-
|
|
23265
23240
|
def _resolve_field_selector_with_uniqueness(
|
|
23266
23241
|
*,
|
|
23267
23242
|
fields: list[dict[str, Any]],
|
|
@@ -23277,12 +23252,7 @@ def _resolve_field_selector_with_uniqueness(
|
|
|
23277
23252
|
name = str(selector.name or "").strip()
|
|
23278
23253
|
matched = [field for field in fields if str(field.get("name") or "").strip() == name]
|
|
23279
23254
|
if len(matched) > 1:
|
|
23280
|
-
|
|
23281
|
-
ref
|
|
23282
|
-
for candidate in matched
|
|
23283
|
-
if (ref := _coerce_any_int(candidate.get("que_id"))) is not None
|
|
23284
|
-
]
|
|
23285
|
-
raise AmbiguousFieldSelector(location, candidate_ids)
|
|
23255
|
+
raise ValueError(f"{location} matched multiple fields with the same name; use field_id or que_id")
|
|
23286
23256
|
else:
|
|
23287
23257
|
matched = []
|
|
23288
23258
|
if len(matched) != 1:
|
|
@@ -29962,14 +29932,6 @@ def _build_view_update_payload(
|
|
|
29962
29932
|
payload["viewgraphType"] = "calendarView"
|
|
29963
29933
|
payload["beingShowTitleQue"] = True
|
|
29964
29934
|
payload.pop("groupQueId", None)
|
|
29965
|
-
elif normalized_type == "hierarchy":
|
|
29966
|
-
payload["viewgraphType"] = "hierarchyView"
|
|
29967
|
-
payload["beingShowTitleQue"] = True
|
|
29968
|
-
payload.pop("groupQueId", None)
|
|
29969
|
-
elif normalized_type == "calendar":
|
|
29970
|
-
payload["viewgraphType"] = "calendarView"
|
|
29971
|
-
payload["beingShowTitleQue"] = True
|
|
29972
|
-
payload.pop("groupQueId", None)
|
|
29973
29935
|
return _hydrate_view_backend_payload(
|
|
29974
29936
|
payload=payload,
|
|
29975
29937
|
view_type=normalized_type,
|
|
@@ -7,22 +7,11 @@ import sys
|
|
|
7
7
|
from typing import Any, Callable, TextIO
|
|
8
8
|
|
|
9
9
|
from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
|
|
10
|
-
from ..public_surface import
|
|
11
|
-
NON_TOOL_CLI_HANDLER_ROUTES,
|
|
12
|
-
cli_public_tool_spec_from_namespace,
|
|
13
|
-
cli_route_from_namespace,
|
|
14
|
-
cli_trim_key_from_namespace,
|
|
15
|
-
)
|
|
10
|
+
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
16
11
|
from ..resource_uri import cli_resource_base_url
|
|
17
12
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
18
13
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
19
14
|
from ..version import get_cli_version, get_cli_version_info
|
|
20
|
-
from ..tool_policy import (
|
|
21
|
-
TOOL_POLICY_ENV,
|
|
22
|
-
TOOL_POLICY_INVALID,
|
|
23
|
-
ToolPolicyViolation,
|
|
24
|
-
parse_tool_policy,
|
|
25
|
-
)
|
|
26
15
|
from .context import CliContext, build_cli_context
|
|
27
16
|
from .formatters import emit_json_result, emit_text_result
|
|
28
17
|
from .commands import register_all_commands
|
|
@@ -101,14 +90,6 @@ def run(
|
|
|
101
90
|
if handler is None:
|
|
102
91
|
parser.print_help(out)
|
|
103
92
|
return 2
|
|
104
|
-
violation = _cli_tool_policy_violation(args)
|
|
105
|
-
if violation is not None:
|
|
106
|
-
return _emit_error(
|
|
107
|
-
violation.payload(),
|
|
108
|
-
json_mode=bool(args.json),
|
|
109
|
-
stdout=out,
|
|
110
|
-
stderr=err,
|
|
111
|
-
)
|
|
112
93
|
try:
|
|
113
94
|
if _should_force_json_output(args):
|
|
114
95
|
setattr(args, "json", True)
|
|
@@ -141,20 +122,6 @@ def run(
|
|
|
141
122
|
return exit_code
|
|
142
123
|
|
|
143
124
|
|
|
144
|
-
def _cli_tool_policy_violation(args: argparse.Namespace) -> ToolPolicyViolation | None:
|
|
145
|
-
stable_id = cli_trim_key_from_namespace(args)
|
|
146
|
-
if stable_id is None:
|
|
147
|
-
# 仅显式登记的非业务分发器可跳过闸门;无子命令的新增 handler 也必须默认拒绝。
|
|
148
|
-
if cli_route_from_namespace(args) in NON_TOOL_CLI_HANDLER_ROUTES:
|
|
149
|
-
return None
|
|
150
|
-
return ToolPolicyViolation(
|
|
151
|
-
TOOL_POLICY_INVALID,
|
|
152
|
-
"user:unregistered",
|
|
153
|
-
"CLI 业务命令未登记稳定工具 ID",
|
|
154
|
-
)
|
|
155
|
-
return parse_tool_policy(os.getenv(TOOL_POLICY_ENV)).violation_for(stable_id)
|
|
156
|
-
|
|
157
|
-
|
|
158
125
|
def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
|
|
159
126
|
info = get_cli_version_info()
|
|
160
127
|
return {
|
|
@@ -2,14 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from contextvars import ContextVar
|
|
4
4
|
from dataclasses import dataclass, field
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import Callable
|
|
6
6
|
|
|
7
7
|
from .backend_client import BackendRequestContext
|
|
8
8
|
from .session_store import BackendSession, SessionProfile
|
|
9
9
|
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from .tool_policy import ToolPolicySnapshot
|
|
12
|
-
|
|
13
10
|
|
|
14
11
|
CLOUD_PROFILE_NAME = "__cloud_mcp__"
|
|
15
12
|
|
|
@@ -76,15 +73,3 @@ current_call_platform: ContextVar[str] = ContextVar(
|
|
|
76
73
|
"qingflow_mcp_current_call_platform",
|
|
77
74
|
default="",
|
|
78
75
|
)
|
|
79
|
-
|
|
80
|
-
# 远端 MCP 每个 HTTP 请求独立绑定策略原文;缺头用 None 表示,不回退进程环境变量。
|
|
81
|
-
current_tool_policy_header: ContextVar[str | None] = ContextVar(
|
|
82
|
-
"qingflow_mcp_current_tool_policy_header",
|
|
83
|
-
default=None,
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
# 在 HTTP 入口合成一次快照,列表与调用共用,避免将 URL 选择保存到会话或共享环境。
|
|
87
|
-
current_http_tool_policy: ContextVar[ToolPolicySnapshot | None] = ContextVar(
|
|
88
|
-
"qingflow_mcp_current_http_tool_policy",
|
|
89
|
-
default=None,
|
|
90
|
-
)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import argparse
|
|
4
3
|
from argparse import Namespace
|
|
5
4
|
from dataclasses import dataclass
|
|
6
5
|
|
|
@@ -110,7 +109,6 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
110
109
|
PublicToolSpec(USER_DOMAIN, "record_export_direct", ("record_export_direct",), ("export", "direct"), cli_show_effective_context=True),
|
|
111
110
|
PublicToolSpec(USER_DOMAIN, "record_code_block_run", ("record_code_block_run",), ("record", "code-block-run"), cli_show_effective_context=True, cli_context_write=True),
|
|
112
111
|
PublicToolSpec(USER_DOMAIN, "task_list", ("task_list",), ("task", "list"), cli_show_effective_context=True),
|
|
113
|
-
PublicToolSpec(USER_DOMAIN, "task_workbench", cli_route=("task",), mcp_public=False),
|
|
114
112
|
PublicToolSpec(USER_DOMAIN, "task_get", ("task_get",), ("task", "get"), cli_show_effective_context=True),
|
|
115
113
|
PublicToolSpec(USER_DOMAIN, "task_action_execute", ("task_action_execute",), ("task", "action"), cli_show_effective_context=True, cli_context_write=True),
|
|
116
114
|
PublicToolSpec(
|
|
@@ -132,12 +130,12 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
132
130
|
|
|
133
131
|
|
|
134
132
|
BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
135
|
-
PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), cli_public=False),
|
|
136
|
-
PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), cli_public=False),
|
|
137
|
-
PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), cli_public=False),
|
|
133
|
+
PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("builder", "auth", "use-credential"), cli_public=False),
|
|
134
|
+
PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
|
|
135
|
+
PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
|
|
138
136
|
PublicToolSpec(BUILDER_DOMAIN, "get_login_link", ("get_login_link",), mcp_public=True, cli_public=False),
|
|
139
|
-
PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), cli_public=False),
|
|
140
|
-
PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), cli_public=False),
|
|
137
|
+
PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
|
|
138
|
+
PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), ("builder", "workspace", "get"), cli_public=False),
|
|
141
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),
|
|
142
140
|
PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
|
|
143
141
|
PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
|
|
@@ -196,8 +194,7 @@ PUBLIC_TOOL_BY_KEY: dict[str, PublicToolSpec] = {spec.trim_key: spec for spec in
|
|
|
196
194
|
PUBLIC_TOOL_BY_CLI_ROUTE: dict[tuple[str, ...], PublicToolSpec] = {
|
|
197
195
|
spec.cli_route: spec
|
|
198
196
|
for spec in ALL_PUBLIC_TOOL_SPECS
|
|
199
|
-
|
|
200
|
-
if spec.cli_route is not None
|
|
197
|
+
if spec.cli_public and spec.cli_route is not None
|
|
201
198
|
}
|
|
202
199
|
PUBLIC_TOOL_BY_CLI_ROUTE.update(
|
|
203
200
|
{
|
|
@@ -208,9 +205,6 @@ PUBLIC_TOOL_BY_CLI_ROUTE.update(
|
|
|
208
205
|
}
|
|
209
206
|
)
|
|
210
207
|
|
|
211
|
-
# 这些 handler 只负责版本输出或参数迁移提示,不会发起轻流业务请求。
|
|
212
|
-
NON_TOOL_CLI_HANDLER_ROUTES = frozenset({("version",), ("record", "schema")})
|
|
213
|
-
|
|
214
208
|
|
|
215
209
|
def public_tool_specs(domain: str | None = None) -> tuple[PublicToolSpec, ...]:
|
|
216
210
|
if domain is None:
|
|
@@ -264,13 +258,11 @@ def cli_route_from_namespace(args: Namespace) -> tuple[str, ...] | None:
|
|
|
264
258
|
record_command = getattr(args, "record_command", None)
|
|
265
259
|
if record_command == "schema":
|
|
266
260
|
schema_command = getattr(args, "record_schema_command", None)
|
|
267
|
-
if isinstance(schema_command, str) and schema_command
|
|
268
|
-
return (command, "schema", schema_command)
|
|
269
|
-
return (command, "schema")
|
|
261
|
+
return (command, "schema", schema_command) if isinstance(schema_command, str) and schema_command else None
|
|
270
262
|
return (command, record_command) if isinstance(record_command, str) and record_command else None
|
|
271
263
|
if command == "task":
|
|
272
264
|
task_command = getattr(args, "task_command", None)
|
|
273
|
-
return (command, task_command) if isinstance(task_command, str) and task_command else
|
|
265
|
+
return (command, task_command) if isinstance(task_command, str) and task_command else None
|
|
274
266
|
if command != "builder":
|
|
275
267
|
child_attr = f"{command}_command"
|
|
276
268
|
child = getattr(args, child_attr, None)
|
|
@@ -291,54 +283,3 @@ def cli_route_from_namespace(args: Namespace) -> tuple[str, ...] | None:
|
|
|
291
283
|
child_attr = f"builder_{builder_command.replace('-', '_')}_command"
|
|
292
284
|
child = getattr(args, child_attr, None)
|
|
293
285
|
return ("builder", builder_command, child) if isinstance(child, str) and child else None
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
def discover_cli_handler_routes(
|
|
297
|
-
parser: argparse.ArgumentParser,
|
|
298
|
-
) -> dict[tuple[str, ...], tuple[str, ...]]:
|
|
299
|
-
"""从 argparse 实际 handler 树枚举规范化路由及其实现,包含中间交互入口。"""
|
|
300
|
-
discovered: dict[tuple[str, ...], set[str]] = {}
|
|
301
|
-
|
|
302
|
-
def walk(current: argparse.ArgumentParser, path: tuple[str, ...], inherited: dict[str, object]) -> None:
|
|
303
|
-
attrs = dict(inherited)
|
|
304
|
-
for action in current._actions:
|
|
305
|
-
if isinstance(action, argparse._SubParsersAction):
|
|
306
|
-
continue
|
|
307
|
-
if action.dest != argparse.SUPPRESS and action.default != argparse.SUPPRESS:
|
|
308
|
-
attrs.setdefault(action.dest, action.default)
|
|
309
|
-
attrs.update(current._defaults)
|
|
310
|
-
|
|
311
|
-
handler = current._defaults.get("handler")
|
|
312
|
-
if callable(handler):
|
|
313
|
-
variants = [attrs]
|
|
314
|
-
for action in current._actions:
|
|
315
|
-
if isinstance(action, argparse._SubParsersAction) or action.choices is None:
|
|
316
|
-
continue
|
|
317
|
-
variants.extend({**attrs, action.dest: choice} for choice in action.choices)
|
|
318
|
-
for variant in variants:
|
|
319
|
-
route = cli_route_from_namespace(Namespace(**variant))
|
|
320
|
-
if route is None:
|
|
321
|
-
route = _normalize_cli_parser_path(path)
|
|
322
|
-
if route:
|
|
323
|
-
identity = f"{handler.__module__}:{handler.__qualname__}"
|
|
324
|
-
discovered.setdefault(route, set()).add(identity)
|
|
325
|
-
|
|
326
|
-
for action in current._actions:
|
|
327
|
-
if not isinstance(action, argparse._SubParsersAction):
|
|
328
|
-
continue
|
|
329
|
-
for name, child in action.choices.items():
|
|
330
|
-
next_attrs = dict(attrs)
|
|
331
|
-
next_attrs[action.dest] = name
|
|
332
|
-
walk(child, path + (name,), next_attrs)
|
|
333
|
-
|
|
334
|
-
walk(parser, (), {})
|
|
335
|
-
return {route: tuple(sorted(handlers)) for route, handlers in discovered.items()}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
def _normalize_cli_parser_path(path: tuple[str, ...]) -> tuple[str, ...]:
|
|
339
|
-
normalized = list(path)
|
|
340
|
-
if normalized and normalized[0] == "build":
|
|
341
|
-
normalized[0] = "builder"
|
|
342
|
-
if len(normalized) > 1 and normalized[0] == "builder" and normalized[1] == "associated-resources":
|
|
343
|
-
normalized[1] = "associated-resource"
|
|
344
|
-
return tuple(normalized)
|
|
@@ -51,9 +51,8 @@ COMMON_ERROR_DROP_TOP = {
|
|
|
51
51
|
|
|
52
52
|
SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
|
|
53
53
|
|
|
54
|
-
#:
|
|
54
|
+
#: 这两个校验命令的产出契约明确包含 ok 字段(供阶段 2 SKILL.md 引用),
|
|
55
55
|
#: 需在 success 裁剪时保留 ok(默认 COMMON_SUCCESS_DROP_TOP 会丢弃 ok)。
|
|
56
|
-
#: `builder:app_views_validate`(views 声明式校验)同属校验类工具,ok 同样进产出契约。
|
|
57
56
|
_KEEP_OK_TOOLS = {
|
|
58
57
|
"builder:app_form_validate_formula",
|
|
59
58
|
"builder:app_form_validate_code_block",
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from datetime import date
|
|
4
4
|
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
|
|
5
7
|
from .backend_client import BackendClient
|
|
6
8
|
from .session_store import SessionStore
|
|
7
9
|
from .tools.app_tools import AppTools
|
|
@@ -24,11 +26,9 @@ from .tools.task_context_tools import TaskContextTools
|
|
|
24
26
|
from .tools.view_tools import ViewTools
|
|
25
27
|
from .tools.workflow_tools import WorkflowTools
|
|
26
28
|
from .tools.workspace_tools import WorkspaceTools
|
|
27
|
-
from .tool_catalog import stdio_server_bindings
|
|
28
|
-
from .tool_policy import PolicyAwareFastMCP, configure_tool_policy
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def _register_missing_builder_tools(server:
|
|
31
|
+
def _register_missing_builder_tools(server: FastMCP) -> None:
|
|
32
32
|
"""Add current high-level builder tools to the unified MCP without duplicate names."""
|
|
33
33
|
from .server_app_builder import build_builder_server
|
|
34
34
|
|
|
@@ -48,10 +48,10 @@ def _register_missing_builder_tools(server: PolicyAwareFastMCP) -> None:
|
|
|
48
48
|
)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
-
def build_server() ->
|
|
51
|
+
def build_server() -> FastMCP:
|
|
52
52
|
today = date.today()
|
|
53
53
|
current_year = today.year
|
|
54
|
-
server =
|
|
54
|
+
server = FastMCP(
|
|
55
55
|
"Qingflow MCP",
|
|
56
56
|
instructions=f"""Use this unified server for Qingflow operational and builder workflows. Current date: `{today.isoformat()}`.
|
|
57
57
|
|
|
@@ -259,14 +259,8 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
259
259
|
WorkflowTools(sessions, backend).register(server)
|
|
260
260
|
ViewTools(sessions, backend).register(server)
|
|
261
261
|
SolutionTools(sessions, backend).register(server)
|
|
262
|
-
# 保存补齐 Builder 前的真实注册来源,裸名冲突只能绑定最终保留 callable 的来源域。
|
|
263
|
-
legacy_tool_names = set(server._tool_manager._tools)
|
|
264
262
|
_register_missing_builder_tools(server)
|
|
265
|
-
return
|
|
266
|
-
server,
|
|
267
|
-
stdio_server_bindings(legacy_tool_names, server._tool_manager._tools),
|
|
268
|
-
source="environment",
|
|
269
|
-
)
|
|
263
|
+
return server
|
|
270
264
|
|
|
271
265
|
|
|
272
266
|
mcp = build_server()
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
|
|
5
7
|
from .backend_client import BackendClient
|
|
6
8
|
from .config import DEFAULT_PROFILE
|
|
7
9
|
from .response_trim import BUILDER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
@@ -12,8 +14,6 @@ from .tools.auth_tools import AuthTools
|
|
|
12
14
|
from .tools.feedback_tools import FeedbackTools
|
|
13
15
|
from .tools.file_tools import FileTools
|
|
14
16
|
from .tools.workspace_tools import WorkspaceTools
|
|
15
|
-
from .tool_catalog import builder_server_bindings
|
|
16
|
-
from .tool_policy import PolicyAwareFastMCP, configure_tool_policy
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def _config_failure(message: str, *, fix_hint: str) -> dict:
|
|
@@ -27,8 +27,8 @@ def _config_failure(message: str, *, fix_hint: str) -> dict:
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
def build_builder_server() ->
|
|
31
|
-
server =
|
|
30
|
+
def build_builder_server() -> FastMCP:
|
|
31
|
+
server = FastMCP(
|
|
32
32
|
"Qingflow App Builder MCP",
|
|
33
33
|
instructions=(
|
|
34
34
|
"Use this server for AI-native Qingflow builder workflows. "
|
|
@@ -689,11 +689,7 @@ def build_builder_server() -> PolicyAwareFastMCP:
|
|
|
689
689
|
expected_package_id=expected_package_id,
|
|
690
690
|
)
|
|
691
691
|
|
|
692
|
-
return
|
|
693
|
-
server,
|
|
694
|
-
builder_server_bindings(server._tool_manager._tools),
|
|
695
|
-
source="environment",
|
|
696
|
-
)
|
|
692
|
+
return server
|
|
697
693
|
|
|
698
694
|
|
|
699
695
|
def main() -> None:
|
|
@@ -3,6 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
import json
|
|
4
4
|
from datetime import date
|
|
5
5
|
|
|
6
|
+
from mcp.server.fastmcp import FastMCP
|
|
7
|
+
|
|
6
8
|
from .backend_client import BackendClient
|
|
7
9
|
from .config import DEFAULT_PROFILE
|
|
8
10
|
from .response_trim import USER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
@@ -20,14 +22,12 @@ from .tools.login_link_tools import LOGIN_LINK_DESCRIPTION, LoginLinkTools
|
|
|
20
22
|
from .tools.resource_read_tools import ResourceReadTools
|
|
21
23
|
from .tools.task_context_tools import TaskContextTools
|
|
22
24
|
from .tools.workspace_tools import WorkspaceTools
|
|
23
|
-
from .tool_catalog import user_server_bindings
|
|
24
|
-
from .tool_policy import PolicyAwareFastMCP, configure_tool_policy
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def build_user_server(*, stateless_http: bool = False) ->
|
|
27
|
+
def build_user_server(*, stateless_http: bool = False) -> FastMCP:
|
|
28
28
|
today = date.today()
|
|
29
29
|
current_year = today.year
|
|
30
|
-
server =
|
|
30
|
+
server = FastMCP(
|
|
31
31
|
"Qingflow App User MCP",
|
|
32
32
|
stateless_http=stateless_http,
|
|
33
33
|
instructions=f"""Use this server for Qingflow operational workflows. Current date: `{today.isoformat()}`.
|
|
@@ -507,11 +507,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
507
507
|
task_context_tools.register(server)
|
|
508
508
|
directory_tools.register_frontend_search(server)
|
|
509
509
|
|
|
510
|
-
return
|
|
511
|
-
server,
|
|
512
|
-
user_server_bindings(server._tool_manager._tools),
|
|
513
|
-
source="request" if stateless_http else "environment",
|
|
514
|
-
)
|
|
510
|
+
return server
|
|
515
511
|
|
|
516
512
|
|
|
517
513
|
mcp = build_user_server()
|
|
@@ -6,15 +6,11 @@ from typing import Any
|
|
|
6
6
|
|
|
7
7
|
import anyio
|
|
8
8
|
import uvicorn
|
|
9
|
+
from mcp.server.fastmcp import FastMCP
|
|
10
|
+
|
|
9
11
|
from .backend_client import BackendClient
|
|
10
12
|
from .cloud_auth import CloudAuthBroker
|
|
11
|
-
from .cloud_context import
|
|
12
|
-
CloudMcpContext,
|
|
13
|
-
current_call_platform,
|
|
14
|
-
current_cloud_context,
|
|
15
|
-
current_http_tool_policy,
|
|
16
|
-
current_tool_policy_header,
|
|
17
|
-
)
|
|
13
|
+
from .cloud_context import CloudMcpContext, current_call_platform, current_cloud_context
|
|
18
14
|
from .cloud_oauth_proxy import (
|
|
19
15
|
CloudOAuthProxyConfig,
|
|
20
16
|
OAuthAccessTokenResolver,
|
|
@@ -25,8 +21,6 @@ from .config import get_cloud_http_allowed_hosts, get_cloud_http_host, get_cloud
|
|
|
25
21
|
from .errors import QingflowApiError, is_auth_like_error
|
|
26
22
|
from .server_app_builder import build_builder_server
|
|
27
23
|
from .server_app_user import build_user_server
|
|
28
|
-
from .tool_catalog import combined_server_bindings
|
|
29
|
-
from .tool_policy import PolicyAwareFastMCP, configure_tool_policy, parse_http_tool_policy
|
|
30
24
|
|
|
31
25
|
|
|
32
26
|
BUILDER_TOOL_PREFIX = "builder_"
|
|
@@ -170,18 +164,12 @@ class CloudMcpAuthApp:
|
|
|
170
164
|
cloud_context.token,
|
|
171
165
|
)
|
|
172
166
|
|
|
173
|
-
tool_policy_header = _extract_header(scope, "x-qingflow-tool-policy")
|
|
174
|
-
http_tool_policy = parse_http_tool_policy(tool_policy_header, bytes(scope.get("query_string") or b""))
|
|
175
167
|
context_token = current_cloud_context.set(cloud_context)
|
|
176
168
|
call_platform_token = current_call_platform.set(call_platform)
|
|
177
|
-
tool_policy_token = current_tool_policy_header.set(tool_policy_header)
|
|
178
|
-
http_tool_policy_token = current_http_tool_policy.set(http_tool_policy)
|
|
179
169
|
try:
|
|
180
170
|
scope = _ensure_streamable_http_accept(scope)
|
|
181
171
|
await self._app(scope, receive, send)
|
|
182
172
|
finally:
|
|
183
|
-
current_http_tool_policy.reset(http_tool_policy_token)
|
|
184
|
-
current_tool_policy_header.reset(tool_policy_token)
|
|
185
173
|
current_call_platform.reset(call_platform_token)
|
|
186
174
|
current_cloud_context.reset(context_token)
|
|
187
175
|
|
|
@@ -409,29 +397,16 @@ def build_cloud_app() -> CloudMcpAuthApp:
|
|
|
409
397
|
)
|
|
410
398
|
|
|
411
399
|
|
|
412
|
-
def build_combined_server() ->
|
|
400
|
+
def build_combined_server() -> FastMCP:
|
|
413
401
|
server = build_user_server(stateless_http=True)
|
|
414
402
|
builder_server = build_builder_server()
|
|
415
|
-
user_tool_names = set(server._tool_manager._tools)
|
|
416
|
-
builder_tool_names = set(builder_server._tool_manager._tools)
|
|
417
403
|
server._mcp_server.name = "Qingflow Combined MCP"
|
|
418
404
|
_register_prefixed_builder_tools(server, builder_server)
|
|
419
405
|
_set_combined_instructions(server, builder_server)
|
|
420
|
-
return
|
|
421
|
-
server,
|
|
422
|
-
combined_server_bindings(
|
|
423
|
-
user_tool_names,
|
|
424
|
-
builder_tool_names,
|
|
425
|
-
builder_prefix=BUILDER_TOOL_PREFIX,
|
|
426
|
-
),
|
|
427
|
-
source="request",
|
|
428
|
-
)
|
|
406
|
+
return server
|
|
429
407
|
|
|
430
408
|
|
|
431
|
-
def _register_prefixed_builder_tools(
|
|
432
|
-
target_server: PolicyAwareFastMCP,
|
|
433
|
-
builder_server: PolicyAwareFastMCP,
|
|
434
|
-
) -> None:
|
|
409
|
+
def _register_prefixed_builder_tools(target_server: FastMCP, builder_server: FastMCP) -> None:
|
|
435
410
|
for original_name, tool in builder_server._tool_manager._tools.items():
|
|
436
411
|
target_server._tool_manager.add_tool(
|
|
437
412
|
tool.fn,
|
|
@@ -444,10 +419,7 @@ def _register_prefixed_builder_tools(
|
|
|
444
419
|
)
|
|
445
420
|
|
|
446
421
|
|
|
447
|
-
def _set_combined_instructions(
|
|
448
|
-
user_server: PolicyAwareFastMCP,
|
|
449
|
-
builder_server: PolicyAwareFastMCP,
|
|
450
|
-
) -> None:
|
|
422
|
+
def _set_combined_instructions(user_server: FastMCP, builder_server: FastMCP) -> None:
|
|
451
423
|
user_instructions = user_server.instructions or ""
|
|
452
424
|
builder_instructions = builder_server.instructions or ""
|
|
453
425
|
user_server._mcp_server.instructions = f"""Use this combined Qingflow MCP server for both user operational workflows and builder workflows.
|
|
@@ -482,14 +454,6 @@ def _extract_host(scope: dict[str, Any]) -> str:
|
|
|
482
454
|
return ""
|
|
483
455
|
|
|
484
456
|
|
|
485
|
-
def _extract_header(scope: dict[str, Any], name: str) -> str | None:
|
|
486
|
-
expected = name.lower().encode("latin1")
|
|
487
|
-
for key, value in scope.get("headers", []):
|
|
488
|
-
if key.lower() == expected:
|
|
489
|
-
return value.decode("utf-8", errors="replace")
|
|
490
|
-
return None
|
|
491
|
-
|
|
492
|
-
|
|
493
457
|
async def _read_request_body(
|
|
494
458
|
receive: Callable[[], Awaitable[dict[str, Any]]],
|
|
495
459
|
limit: int,
|