@qingflow-tech/qingflow-app-user-mcp 1.0.9 → 1.0.10
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 +2 -2
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-record-update/SKILL.md +2 -0
- package/src/qingflow_mcp/builder_facade/models.py +2 -0
- package/src/qingflow_mcp/builder_facade/service.py +223 -39
- package/src/qingflow_mcp/cli/commands/builder.py +40 -11
- package/src/qingflow_mcp/cli/main.py +204 -3
- package/src/qingflow_mcp/response_trim.py +15 -10
- package/src/qingflow_mcp/server_app_builder.py +27 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +1189 -29
- package/src/qingflow_mcp/tools/record_tools.py +200 -6
|
@@ -84,7 +84,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
84
84
|
|
|
85
85
|
package_apply = package_subparsers.add_parser("apply", help="创建或更新应用包配置")
|
|
86
86
|
package_apply.add_argument("--config-file", required=True)
|
|
87
|
-
package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary")
|
|
87
|
+
package_apply.set_defaults(handler=_handle_package_apply, format_hint="builder_summary", force_json_output=True)
|
|
88
88
|
|
|
89
89
|
app = builder_subparsers.add_parser("app", help="应用")
|
|
90
90
|
app_subparsers = app.add_subparsers(dest="builder_app_command", required=True)
|
|
@@ -99,7 +99,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
99
99
|
app_release_lock.add_argument("--app-key", required=True)
|
|
100
100
|
app_release_lock.add_argument("--lock-owner-email", required=True)
|
|
101
101
|
app_release_lock.add_argument("--lock-owner-name", required=True)
|
|
102
|
-
app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary")
|
|
102
|
+
app_release_lock.set_defaults(handler=_handle_app_release_edit_lock_if_mine, format_hint="builder_summary", force_json_output=True)
|
|
103
103
|
|
|
104
104
|
app_get = app_subparsers.add_parser("get", help="读取应用配置(字段请使用: builder app get --app-key APP fields)")
|
|
105
105
|
app_get.add_argument(
|
|
@@ -129,7 +129,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
129
129
|
button_apply.add_argument("--patch-buttons-file")
|
|
130
130
|
button_apply.add_argument("--remove-buttons-file")
|
|
131
131
|
button_apply.add_argument("--view-configs-file")
|
|
132
|
-
button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary")
|
|
132
|
+
button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary", force_json_output=True)
|
|
133
133
|
|
|
134
134
|
associated_resource = builder_subparsers.add_parser("associated-resource", aliases=["associated-resources"], help="关联视图/报表")
|
|
135
135
|
associated_resource_subparsers = associated_resource.add_subparsers(dest="builder_associated_resource_command", required=True)
|
|
@@ -140,7 +140,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
140
140
|
associated_resource_apply.add_argument("--remove-associated-item-ids-file")
|
|
141
141
|
associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
|
|
142
142
|
associated_resource_apply.add_argument("--view-configs-file")
|
|
143
|
-
associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary")
|
|
143
|
+
associated_resource_apply.set_defaults(handler=_handle_associated_resource_apply, format_hint="builder_summary", force_json_output=True)
|
|
144
144
|
|
|
145
145
|
portal = builder_subparsers.add_parser("portal", help="门户")
|
|
146
146
|
portal_subparsers = portal.add_subparsers(dest="builder_portal_command", required=True)
|
|
@@ -165,7 +165,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
165
165
|
portal_apply.add_argument("--hide-copyright", action=argparse.BooleanOptionalAction, default=None)
|
|
166
166
|
portal_apply.add_argument("--dash-global-config-file")
|
|
167
167
|
portal_apply.add_argument("--config-file")
|
|
168
|
-
portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary")
|
|
168
|
+
portal_apply.set_defaults(handler=_handle_portal_apply, format_hint="builder_summary", force_json_output=True)
|
|
169
169
|
|
|
170
170
|
schema_apply = builder_subparsers.add_parser("schema", help="字段搭建")
|
|
171
171
|
schema_apply_subparsers = schema_apply.add_subparsers(dest="builder_schema_command", required=True)
|
|
@@ -179,10 +179,11 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
179
179
|
schema_apply_apply.add_argument("--visibility-file")
|
|
180
180
|
schema_apply_apply.add_argument("--create-if-missing", action="store_true")
|
|
181
181
|
schema_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
182
|
+
schema_apply_apply.add_argument("--apps-file", help="多应用 schema JSON 数组;每项可带 client_key/app_name/add_fields,支持 relation target_app_ref")
|
|
182
183
|
schema_apply_apply.add_argument("--add-fields-file", help="字段 JSON 数组;字段可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
183
184
|
schema_apply_apply.add_argument("--update-fields-file", help="字段更新 JSON 数组;set 内可用 as_data_title/as_data_cover 标记数据标题/封面")
|
|
184
185
|
schema_apply_apply.add_argument("--remove-fields-file")
|
|
185
|
-
schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary")
|
|
186
|
+
schema_apply_apply.set_defaults(handler=_handle_schema_apply, format_hint="builder_summary", force_json_output=True)
|
|
186
187
|
|
|
187
188
|
layout_apply = builder_subparsers.add_parser("layout", help="布局")
|
|
188
189
|
layout_apply_subparsers = layout_apply.add_subparsers(dest="builder_layout_command", required=True)
|
|
@@ -191,7 +192,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
191
192
|
layout_apply_apply.add_argument("--mode", choices=["merge", "replace"], default="merge")
|
|
192
193
|
layout_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
193
194
|
layout_apply_apply.add_argument("--sections-file", required=True)
|
|
194
|
-
layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary")
|
|
195
|
+
layout_apply_apply.set_defaults(handler=_handle_layout_apply, format_hint="builder_summary", force_json_output=True)
|
|
195
196
|
|
|
196
197
|
views_apply = builder_subparsers.add_parser("views", help="视图")
|
|
197
198
|
views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
|
|
@@ -201,7 +202,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
201
202
|
views_apply_apply.add_argument("--upsert-views-file")
|
|
202
203
|
views_apply_apply.add_argument("--patch-views-file")
|
|
203
204
|
views_apply_apply.add_argument("--remove-views-file")
|
|
204
|
-
views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary")
|
|
205
|
+
views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary", force_json_output=True)
|
|
205
206
|
|
|
206
207
|
flow_apply = builder_subparsers.add_parser("flow", help="流程")
|
|
207
208
|
flow_apply_subparsers = flow_apply.add_subparsers(dest="builder_flow_command", required=True)
|
|
@@ -211,7 +212,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
211
212
|
flow_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
212
213
|
flow_apply_apply.add_argument("--nodes-file", required=True)
|
|
213
214
|
flow_apply_apply.add_argument("--transitions-file", required=True)
|
|
214
|
-
flow_apply_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary")
|
|
215
|
+
flow_apply_apply.set_defaults(handler=_handle_flow_apply, format_hint="builder_summary", force_json_output=True)
|
|
215
216
|
|
|
216
217
|
charts_apply = builder_subparsers.add_parser("charts", help="报表")
|
|
217
218
|
charts_apply_subparsers = charts_apply.add_subparsers(dest="builder_charts_command", required=True)
|
|
@@ -221,14 +222,14 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
221
222
|
charts_apply_apply.add_argument("--patch-file")
|
|
222
223
|
charts_apply_apply.add_argument("--remove-chart-ids-file")
|
|
223
224
|
charts_apply_apply.add_argument("--reorder-chart-ids-file")
|
|
224
|
-
charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary")
|
|
225
|
+
charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary", force_json_output=True)
|
|
225
226
|
|
|
226
227
|
publish_verify = builder_subparsers.add_parser("publish", help="发布校验")
|
|
227
228
|
publish_verify_subparsers = publish_verify.add_subparsers(dest="builder_publish_command", required=True)
|
|
228
229
|
publish_verify_verify = publish_verify_subparsers.add_parser("verify", help="校验应用发布")
|
|
229
230
|
publish_verify_verify.add_argument("--app-key", required=True)
|
|
230
231
|
publish_verify_verify.add_argument("--expected-package-id", type=int)
|
|
231
|
-
publish_verify_verify.set_defaults(handler=_handle_publish_verify, format_hint="builder_summary")
|
|
232
|
+
publish_verify_verify.set_defaults(handler=_handle_publish_verify, format_hint="builder_summary", force_json_output=True)
|
|
232
233
|
|
|
233
234
|
view = builder_subparsers.add_parser("view", help="视图详情")
|
|
234
235
|
view_subparsers = view.add_subparsers(dest="builder_view_command", required=True)
|
|
@@ -451,6 +452,34 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
451
452
|
|
|
452
453
|
|
|
453
454
|
def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
455
|
+
apps = load_list_arg(args.apps_file, option_name="--apps-file")
|
|
456
|
+
if args.apps_file:
|
|
457
|
+
if not apps:
|
|
458
|
+
raise_config_error(
|
|
459
|
+
"schema apply multi-app mode requires a non-empty --apps-file.",
|
|
460
|
+
fix_hint="Pass a JSON array with at least one app item.",
|
|
461
|
+
)
|
|
462
|
+
if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
|
|
463
|
+
raise_config_error(
|
|
464
|
+
"schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
|
|
465
|
+
fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
|
|
466
|
+
)
|
|
467
|
+
if args.package_id is None:
|
|
468
|
+
raise_config_error(
|
|
469
|
+
"schema apply multi-app mode requires --package-id.",
|
|
470
|
+
fix_hint="Pass `--package-id` and app names inside --apps-file.",
|
|
471
|
+
)
|
|
472
|
+
return context.builder.app_schema_apply(
|
|
473
|
+
profile=args.profile,
|
|
474
|
+
package_id=args.package_id,
|
|
475
|
+
visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
|
|
476
|
+
create_if_missing=bool(args.create_if_missing),
|
|
477
|
+
publish=bool(args.publish),
|
|
478
|
+
apps=apps,
|
|
479
|
+
add_fields=[],
|
|
480
|
+
update_fields=[],
|
|
481
|
+
remove_fields=[],
|
|
482
|
+
)
|
|
454
483
|
has_app_key = bool((args.app_key or "").strip())
|
|
455
484
|
has_app_name = bool((args.app_name or "").strip())
|
|
456
485
|
has_app_title = bool((args.app_title or "").strip())
|
|
@@ -8,6 +8,7 @@ from typing import Any, Callable, TextIO
|
|
|
8
8
|
from ..errors import QingflowApiError
|
|
9
9
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
10
10
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
|
+
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
11
12
|
from .context import CliContext, build_cli_context
|
|
12
13
|
from .formatters import emit_json_result, emit_text_result
|
|
13
14
|
from .commands import register_all_commands
|
|
@@ -16,8 +17,21 @@ from .commands import register_all_commands
|
|
|
16
17
|
Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
|
|
17
18
|
|
|
18
19
|
|
|
20
|
+
class _CliArgumentError(Exception):
|
|
21
|
+
def __init__(self, *, prog: str, message: str, usage: str) -> None:
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
self.prog = prog
|
|
24
|
+
self.message = message
|
|
25
|
+
self.usage = usage
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class _QingflowArgumentParser(argparse.ArgumentParser):
|
|
29
|
+
def error(self, message: str) -> None:
|
|
30
|
+
raise _CliArgumentError(prog=self.prog, message=message, usage=self.format_usage())
|
|
31
|
+
|
|
32
|
+
|
|
19
33
|
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
-
parser =
|
|
34
|
+
parser = _QingflowArgumentParser(prog="qingflow", description="Qingflow CLI")
|
|
21
35
|
parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
|
|
22
36
|
parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
23
37
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
@@ -42,6 +56,21 @@ def run(
|
|
|
42
56
|
normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
|
|
43
57
|
try:
|
|
44
58
|
args = parser.parse_args(normalized_argv)
|
|
59
|
+
except _CliArgumentError as exc:
|
|
60
|
+
if _should_force_json_output_argv(normalized_argv):
|
|
61
|
+
payload = {
|
|
62
|
+
"category": "config",
|
|
63
|
+
"status": "failed",
|
|
64
|
+
"error_code": "ARGUMENT_ERROR",
|
|
65
|
+
"message": exc.message,
|
|
66
|
+
"details": {"usage": exc.usage.strip(), "prog": exc.prog},
|
|
67
|
+
}
|
|
68
|
+
payload = _maybe_attach_builder_apply_error_envelope_from_argv(normalized_argv, payload)
|
|
69
|
+
emit_json_result(payload, stream=out)
|
|
70
|
+
return 2
|
|
71
|
+
err.write(exc.usage)
|
|
72
|
+
err.write(f"{exc.prog}: error: {exc.message}\n")
|
|
73
|
+
return 2
|
|
45
74
|
except SystemExit as exc:
|
|
46
75
|
return int(exc.code or 0)
|
|
47
76
|
setattr(args, "_stdin", sys.stdin)
|
|
@@ -51,8 +80,10 @@ def run(
|
|
|
51
80
|
if handler is None:
|
|
52
81
|
parser.print_help(out)
|
|
53
82
|
return 2
|
|
54
|
-
context = context_factory()
|
|
55
83
|
try:
|
|
84
|
+
if _should_force_json_output(args):
|
|
85
|
+
setattr(args, "json", True)
|
|
86
|
+
context = context_factory()
|
|
56
87
|
if not bool(args.json):
|
|
57
88
|
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
58
89
|
result = handler(args, context)
|
|
@@ -60,12 +91,15 @@ def run(
|
|
|
60
91
|
return int(exc.code or 0)
|
|
61
92
|
except RuntimeError as exc:
|
|
62
93
|
payload = trim_error_response(_parse_error_payload(exc))
|
|
94
|
+
payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
|
|
63
95
|
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
64
96
|
except QingflowApiError as exc:
|
|
65
97
|
payload = trim_error_response(exc.to_dict())
|
|
98
|
+
payload = _maybe_attach_builder_apply_error_envelope_from_args(args, payload)
|
|
66
99
|
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
67
100
|
finally:
|
|
68
|
-
context
|
|
101
|
+
if "context" in locals():
|
|
102
|
+
context.close()
|
|
69
103
|
|
|
70
104
|
exit_code = _result_exit_code(result)
|
|
71
105
|
trimmed_result = trim_public_response(resolve_cli_tool_name(args), result) if isinstance(result, dict) else result
|
|
@@ -104,6 +138,173 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
|
104
138
|
return global_args + remaining
|
|
105
139
|
|
|
106
140
|
|
|
141
|
+
def _should_force_json_output(args: argparse.Namespace) -> bool:
|
|
142
|
+
if bool(getattr(args, "force_json_output", False)):
|
|
143
|
+
return True
|
|
144
|
+
if (
|
|
145
|
+
getattr(args, "command", "") == "builder"
|
|
146
|
+
and getattr(args, "builder_app_command", "") == "repair-code-blocks"
|
|
147
|
+
and bool(getattr(args, "apply", False))
|
|
148
|
+
):
|
|
149
|
+
return True
|
|
150
|
+
return False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _should_force_json_output_argv(argv: list[str]) -> bool:
|
|
154
|
+
tokens = _strip_global_args(argv)
|
|
155
|
+
if not tokens or tokens[0] not in {"builder", "build"}:
|
|
156
|
+
return False
|
|
157
|
+
if len(tokens) < 3:
|
|
158
|
+
return False
|
|
159
|
+
section = tokens[1]
|
|
160
|
+
action = tokens[2]
|
|
161
|
+
if section in {"package", "button", "associated-resource", "associated-resources", "portal", "schema", "layout", "views", "flow", "charts"}:
|
|
162
|
+
return action == "apply"
|
|
163
|
+
if section == "publish":
|
|
164
|
+
return action == "verify"
|
|
165
|
+
if section == "app":
|
|
166
|
+
if action == "release-edit-lock-if-mine":
|
|
167
|
+
return True
|
|
168
|
+
if action == "repair-code-blocks":
|
|
169
|
+
return "--apply" in tokens
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _maybe_attach_builder_apply_error_envelope_from_args(args: argparse.Namespace, payload: dict[str, Any]) -> dict[str, Any]:
|
|
174
|
+
operation = _builder_apply_operation_from_args(args)
|
|
175
|
+
if not operation:
|
|
176
|
+
return payload
|
|
177
|
+
enriched = dict(payload)
|
|
178
|
+
enriched.setdefault("status", "failed")
|
|
179
|
+
enriched.setdefault("ok", False)
|
|
180
|
+
enriched.setdefault("write_executed", False)
|
|
181
|
+
enriched.setdefault("safe_to_retry", False)
|
|
182
|
+
_copy_arg_identity(enriched, args)
|
|
183
|
+
return _attach_builder_apply_envelope(operation, enriched)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _maybe_attach_builder_apply_error_envelope_from_argv(argv: list[str], payload: dict[str, Any]) -> dict[str, Any]:
|
|
187
|
+
operation = _builder_apply_operation_from_argv(argv)
|
|
188
|
+
if not operation:
|
|
189
|
+
return payload
|
|
190
|
+
enriched = dict(payload)
|
|
191
|
+
enriched.setdefault("status", "failed")
|
|
192
|
+
enriched.setdefault("ok", False)
|
|
193
|
+
enriched.setdefault("write_executed", False)
|
|
194
|
+
enriched.setdefault("safe_to_retry", False)
|
|
195
|
+
_copy_argv_identity(enriched, argv)
|
|
196
|
+
return _attach_builder_apply_envelope(operation, enriched)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _builder_apply_operation_from_args(args: argparse.Namespace) -> str | None:
|
|
200
|
+
if getattr(args, "command", "") not in {"builder", "build"}:
|
|
201
|
+
return None
|
|
202
|
+
section = str(getattr(args, "builder_command", "") or "")
|
|
203
|
+
if section == "package" and getattr(args, "builder_package_command", "") == "apply":
|
|
204
|
+
return "package_apply"
|
|
205
|
+
if section == "button" and getattr(args, "builder_button_command", "") == "apply":
|
|
206
|
+
return "app_custom_buttons_apply"
|
|
207
|
+
if section in {"associated-resource", "associated-resources"} and getattr(args, "builder_associated_resource_command", "") == "apply":
|
|
208
|
+
return "app_associated_resources_apply"
|
|
209
|
+
if section == "portal" and getattr(args, "builder_portal_command", "") == "apply":
|
|
210
|
+
return "portal_apply"
|
|
211
|
+
if section == "schema" and getattr(args, "builder_schema_command", "") == "apply":
|
|
212
|
+
return "app_schema_apply"
|
|
213
|
+
if section == "layout" and getattr(args, "builder_layout_command", "") == "apply":
|
|
214
|
+
return "app_layout_apply"
|
|
215
|
+
if section == "views" and getattr(args, "builder_views_command", "") == "apply":
|
|
216
|
+
return "app_views_apply"
|
|
217
|
+
if section == "flow" and getattr(args, "builder_flow_command", "") == "apply":
|
|
218
|
+
return "app_flow_apply"
|
|
219
|
+
if section == "charts" and getattr(args, "builder_charts_command", "") == "apply":
|
|
220
|
+
return "app_charts_apply"
|
|
221
|
+
if section == "publish" and getattr(args, "builder_publish_command", "") == "verify":
|
|
222
|
+
return "app_publish_verify"
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _builder_apply_operation_from_argv(argv: list[str]) -> str | None:
|
|
227
|
+
tokens = _strip_global_args(argv)
|
|
228
|
+
if not tokens or tokens[0] not in {"builder", "build"} or len(tokens) < 3:
|
|
229
|
+
return None
|
|
230
|
+
section = tokens[1]
|
|
231
|
+
action = tokens[2]
|
|
232
|
+
if action != "apply" and not (section == "publish" and action == "verify"):
|
|
233
|
+
return None
|
|
234
|
+
mapping = {
|
|
235
|
+
"package": "package_apply",
|
|
236
|
+
"button": "app_custom_buttons_apply",
|
|
237
|
+
"associated-resource": "app_associated_resources_apply",
|
|
238
|
+
"associated-resources": "app_associated_resources_apply",
|
|
239
|
+
"portal": "portal_apply",
|
|
240
|
+
"schema": "app_schema_apply",
|
|
241
|
+
"layout": "app_layout_apply",
|
|
242
|
+
"views": "app_views_apply",
|
|
243
|
+
"flow": "app_flow_apply",
|
|
244
|
+
"charts": "app_charts_apply",
|
|
245
|
+
"publish": "app_publish_verify",
|
|
246
|
+
}
|
|
247
|
+
return mapping.get(section)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _copy_arg_identity(payload: dict[str, Any], args: argparse.Namespace) -> None:
|
|
251
|
+
for attr, key in (
|
|
252
|
+
("app_key", "app_key"),
|
|
253
|
+
("app_name", "app_name"),
|
|
254
|
+
("app_title", "app_title"),
|
|
255
|
+
("package_id", "package_id"),
|
|
256
|
+
("dash_key", "dash_key"),
|
|
257
|
+
("dash_name", "dash_name"),
|
|
258
|
+
):
|
|
259
|
+
value = getattr(args, attr, None)
|
|
260
|
+
if value not in (None, ""):
|
|
261
|
+
payload.setdefault(key, value)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _copy_argv_identity(payload: dict[str, Any], argv: list[str]) -> None:
|
|
265
|
+
tokens = _strip_global_args(argv)
|
|
266
|
+
option_to_key = {
|
|
267
|
+
"--app-key": "app_key",
|
|
268
|
+
"--app-name": "app_name",
|
|
269
|
+
"--app-title": "app_title",
|
|
270
|
+
"--package-id": "package_id",
|
|
271
|
+
"--dash-key": "dash_key",
|
|
272
|
+
"--dash-name": "dash_name",
|
|
273
|
+
}
|
|
274
|
+
index = 0
|
|
275
|
+
while index < len(tokens):
|
|
276
|
+
token = tokens[index]
|
|
277
|
+
if token in option_to_key and index + 1 < len(tokens):
|
|
278
|
+
payload.setdefault(option_to_key[token], tokens[index + 1])
|
|
279
|
+
index += 2
|
|
280
|
+
continue
|
|
281
|
+
for option, key in option_to_key.items():
|
|
282
|
+
prefix = f"{option}="
|
|
283
|
+
if token.startswith(prefix):
|
|
284
|
+
payload.setdefault(key, token[len(prefix) :])
|
|
285
|
+
break
|
|
286
|
+
index += 1
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _strip_global_args(argv: list[str]) -> list[str]:
|
|
290
|
+
stripped: list[str] = []
|
|
291
|
+
index = 0
|
|
292
|
+
while index < len(argv):
|
|
293
|
+
token = argv[index]
|
|
294
|
+
if token == "--json":
|
|
295
|
+
index += 1
|
|
296
|
+
continue
|
|
297
|
+
if token == "--profile":
|
|
298
|
+
index += 2
|
|
299
|
+
continue
|
|
300
|
+
if token.startswith("--profile="):
|
|
301
|
+
index += 1
|
|
302
|
+
continue
|
|
303
|
+
stripped.append(token)
|
|
304
|
+
index += 1
|
|
305
|
+
return stripped
|
|
306
|
+
|
|
307
|
+
|
|
107
308
|
def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
|
|
108
309
|
raw = str(exc)
|
|
109
310
|
try:
|
|
@@ -58,7 +58,7 @@ def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict
|
|
|
58
58
|
return payload
|
|
59
59
|
if _looks_like_failure_payload(payload):
|
|
60
60
|
status = str(payload.get("status") or "").lower()
|
|
61
|
-
if tool_name in {"user:record_insert", "user:record_update"} and status in {
|
|
61
|
+
if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and status in {
|
|
62
62
|
"blocked",
|
|
63
63
|
"needs_confirmation",
|
|
64
64
|
"partial_success",
|
|
@@ -75,6 +75,8 @@ def trim_success_response(tool_name: str | None, payload: dict[str, Any]) -> dic
|
|
|
75
75
|
drop_keys = COMMON_SUCCESS_DROP_TOP
|
|
76
76
|
if tool_name == "user:record_get":
|
|
77
77
|
drop_keys = COMMON_SUCCESS_DROP_TOP - {"output_profile"}
|
|
78
|
+
if tool_name in {"user:record_insert", "user:record_update", "user:record_delete"} and payload.get("ok") is False:
|
|
79
|
+
drop_keys = drop_keys - {"ok"}
|
|
78
80
|
_drop_top_keys(trimmed, drop_keys)
|
|
79
81
|
transformer = SUCCESS_POLICY_BY_TOOL.get(tool_name or "")
|
|
80
82
|
if transformer is not None:
|
|
@@ -420,7 +422,7 @@ def _trim_record_write(payload: JSONObject) -> None:
|
|
|
420
422
|
def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
|
|
421
423
|
data.pop("items", None)
|
|
422
424
|
data.pop("debug", None)
|
|
423
|
-
for key in ("summary", "
|
|
425
|
+
for key in ("summary", "app_key", "mode"):
|
|
424
426
|
if data.get(key) in (None, [], {}, ""):
|
|
425
427
|
data.pop(key, None)
|
|
426
428
|
items = payload.get("items")
|
|
@@ -449,10 +451,8 @@ def _trim_record_write_batch(payload: JSONObject, data: JSONObject) -> None:
|
|
|
449
451
|
for item in items
|
|
450
452
|
if isinstance(item, dict)
|
|
451
453
|
]
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
if value in (None, [], {}, ""):
|
|
455
|
-
payload.pop(key, None)
|
|
454
|
+
if payload.get("items") in (None, [], {}, ""):
|
|
455
|
+
payload.pop("items", None)
|
|
456
456
|
|
|
457
457
|
|
|
458
458
|
def _trim_record_get(payload: JSONObject) -> None:
|
|
@@ -714,13 +714,18 @@ def _trim_record_delete(payload: JSONObject) -> None:
|
|
|
714
714
|
if not isinstance(data, dict):
|
|
715
715
|
return
|
|
716
716
|
resource = data.get("resource")
|
|
717
|
-
deleted_ids
|
|
718
|
-
if isinstance(
|
|
717
|
+
deleted_ids = payload.get("deleted_ids") if isinstance(payload.get("deleted_ids"), list) else data.get("deleted_ids")
|
|
718
|
+
failed_ids = payload.get("failed_ids") if isinstance(payload.get("failed_ids"), list) else data.get("failed_ids")
|
|
719
|
+
if not isinstance(deleted_ids, list):
|
|
720
|
+
deleted_ids = []
|
|
721
|
+
if not isinstance(failed_ids, list):
|
|
722
|
+
failed_ids = []
|
|
723
|
+
if not deleted_ids and isinstance(resource, dict):
|
|
719
724
|
raw_ids = resource.get("record_ids") or resource.get("apply_ids") or resource.get("applyIds")
|
|
720
725
|
if isinstance(raw_ids, list):
|
|
721
726
|
deleted_ids = [str(item) for item in raw_ids if item not in (None, "")]
|
|
722
|
-
data["deleted_ids"] = deleted_ids
|
|
723
|
-
data
|
|
727
|
+
data["deleted_ids"] = [str(item) for item in deleted_ids if item not in (None, "")]
|
|
728
|
+
data["failed_ids"] = [str(item) for item in failed_ids if item not in (None, "")]
|
|
724
729
|
for key in (
|
|
725
730
|
"resource",
|
|
726
731
|
"action",
|
|
@@ -39,9 +39,10 @@ def build_builder_server() -> FastMCP:
|
|
|
39
39
|
"app_get as the default app map read, then app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for focused configuration reads, "
|
|
40
40
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
41
41
|
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_custom_buttons_apply/app_associated_resources_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, app_custom_buttons_apply and app_associated_resources_apply publish after at least one write succeeds and expose no draft-only parameter, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates use replace semantics only when sections are supplied and edit-mode base-info-only updates may omit sections, publish=false only guarantees draft/base-info updates for tools that still expose that parameter, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
42
|
+
"Builder apply/write outputs include schema_version, operation, summary, and resources[]; use resources[].id/key/name/ids/parent as the stable UI and agent display entry, and keep legacy fields such as field_diff/views_diff/chart_results only for compatibility or troubleshooting. "
|
|
42
43
|
"For existing object parameter replacement, prefer patch_views, patch_buttons, patch_resources, and patch_charts with set/unset; the tool reads current config and full-saves internally, while upsert_* is for creation or full target configuration and should not be used as an incomplete partial update. "
|
|
43
|
-
"For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. "
|
|
44
|
-
"For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. "
|
|
44
|
+
"For app_schema_apply, configure data title and data cover directly in field JSON with as_data_title=true and as_data_cover=true; data title is required and exactly one field may be marked, while data cover is optional and must be a top-level attachment field. For multi-app creation, pass apps[]/--apps-file on app_schema_apply; each item may have client_key, and relation fields may use target_app_ref to point at another same-call client_key. "
|
|
45
|
+
"For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows is a matrix of field names compiled to backend queryCondition queIds. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. "
|
|
45
46
|
"For custom button body create/update/delete and view placement, use app_custom_buttons_apply. For addData buttons, prefer trigger_add_data_config.target_app_key + field_mappings/default_values; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default and merge-mode view_configs must include buttons; use view_configs[].mode=replace or explicit buttons=[] only when clearing/replacing existing bindings is intended. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
|
|
46
47
|
"For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
|
|
47
48
|
"For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
|
|
@@ -407,7 +408,30 @@ def build_builder_server() -> FastMCP:
|
|
|
407
408
|
add_fields: list[dict] | None = None,
|
|
408
409
|
update_fields: list[dict] | None = None,
|
|
409
410
|
remove_fields: list[dict] | None = None,
|
|
411
|
+
apps: list[dict] | None = None,
|
|
410
412
|
) -> dict:
|
|
413
|
+
if apps:
|
|
414
|
+
if app_key or app_name or app_title or add_fields or update_fields or remove_fields:
|
|
415
|
+
return _config_failure(
|
|
416
|
+
"app_schema_apply multi-app mode accepts package_id/create_if_missing plus apps only.",
|
|
417
|
+
fix_hint="Use `apps` for batch mode, or use the single-app arguments without `apps`.",
|
|
418
|
+
)
|
|
419
|
+
if package_id is None:
|
|
420
|
+
return _config_failure(
|
|
421
|
+
"app_schema_apply multi-app mode requires package_id.",
|
|
422
|
+
fix_hint="Pass `package_id` and `apps[].app_name` for new apps, or `apps[].app_key` for existing apps.",
|
|
423
|
+
)
|
|
424
|
+
return ai_builder.app_schema_apply(
|
|
425
|
+
profile=profile,
|
|
426
|
+
package_id=package_id,
|
|
427
|
+
visibility=visibility,
|
|
428
|
+
create_if_missing=create_if_missing,
|
|
429
|
+
publish=publish,
|
|
430
|
+
add_fields=[],
|
|
431
|
+
update_fields=[],
|
|
432
|
+
remove_fields=[],
|
|
433
|
+
apps=apps,
|
|
434
|
+
)
|
|
411
435
|
has_app_key = bool((app_key or "").strip())
|
|
412
436
|
has_app_name = bool((app_name or "").strip())
|
|
413
437
|
has_app_title = bool((app_title or "").strip())
|
|
@@ -437,6 +461,7 @@ def build_builder_server() -> FastMCP:
|
|
|
437
461
|
add_fields=add_fields or [],
|
|
438
462
|
update_fields=update_fields or [],
|
|
439
463
|
remove_fields=remove_fields or [],
|
|
464
|
+
apps=[],
|
|
440
465
|
)
|
|
441
466
|
|
|
442
467
|
@server.tool()
|