@qingflow-tech/qingflow-app-builder-mcp 1.0.7 → 1.0.9
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-app-builder/SKILL.md +44 -14
- package/skills/qingflow-app-builder/references/gotchas.md +32 -1
- package/skills/qingflow-app-builder/references/match-rules.md +114 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +10 -6
- package/skills/qingflow-app-builder/references/update-views.md +19 -19
- package/src/qingflow_mcp/builder_facade/models.py +205 -11
- package/src/qingflow_mcp/builder_facade/service.py +2303 -159
- package/src/qingflow_mcp/cli/commands/builder.py +8 -0
- package/src/qingflow_mcp/cli/commands/record.py +55 -1
- package/src/qingflow_mcp/public_surface.py +2 -2
- package/src/qingflow_mcp/response_trim.py +14 -0
- package/src/qingflow_mcp/server.py +1 -0
- package/src/qingflow_mcp/server_app_builder.py +13 -2
- package/src/qingflow_mcp/server_app_user.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +199 -10
|
@@ -126,6 +126,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
126
126
|
button_apply = button_subparsers.add_parser("apply", help="声明式创建、更新或删除自定义按钮")
|
|
127
127
|
button_apply.add_argument("--app-key", required=True)
|
|
128
128
|
button_apply.add_argument("--upsert-buttons-file")
|
|
129
|
+
button_apply.add_argument("--patch-buttons-file")
|
|
129
130
|
button_apply.add_argument("--remove-buttons-file")
|
|
130
131
|
button_apply.add_argument("--view-configs-file")
|
|
131
132
|
button_apply.set_defaults(handler=_handle_button_apply, format_hint="builder_summary")
|
|
@@ -135,6 +136,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
135
136
|
associated_resource_apply = associated_resource_subparsers.add_parser("apply", help="声明式管理应用关联资源池和视图展示配置")
|
|
136
137
|
associated_resource_apply.add_argument("--app-key", required=True)
|
|
137
138
|
associated_resource_apply.add_argument("--upsert-resources-file")
|
|
139
|
+
associated_resource_apply.add_argument("--patch-resources-file")
|
|
138
140
|
associated_resource_apply.add_argument("--remove-associated-item-ids-file")
|
|
139
141
|
associated_resource_apply.add_argument("--reorder-associated-item-ids-file")
|
|
140
142
|
associated_resource_apply.add_argument("--view-configs-file")
|
|
@@ -197,6 +199,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
197
199
|
views_apply_apply.add_argument("--app-key", required=True)
|
|
198
200
|
views_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
|
|
199
201
|
views_apply_apply.add_argument("--upsert-views-file")
|
|
202
|
+
views_apply_apply.add_argument("--patch-views-file")
|
|
200
203
|
views_apply_apply.add_argument("--remove-views-file")
|
|
201
204
|
views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary")
|
|
202
205
|
|
|
@@ -215,6 +218,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
215
218
|
charts_apply_apply = charts_apply_subparsers.add_parser("apply", help="执行报表变更")
|
|
216
219
|
charts_apply_apply.add_argument("--app-key", required=True)
|
|
217
220
|
charts_apply_apply.add_argument("--upsert-file")
|
|
221
|
+
charts_apply_apply.add_argument("--patch-file")
|
|
218
222
|
charts_apply_apply.add_argument("--remove-chart-ids-file")
|
|
219
223
|
charts_apply_apply.add_argument("--reorder-chart-ids-file")
|
|
220
224
|
charts_apply_apply.set_defaults(handler=_handle_charts_apply, format_hint="builder_summary")
|
|
@@ -366,6 +370,7 @@ def _handle_button_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
366
370
|
profile=args.profile,
|
|
367
371
|
app_key=args.app_key,
|
|
368
372
|
upsert_buttons=load_list_arg(args.upsert_buttons_file, option_name="--upsert-buttons-file"),
|
|
373
|
+
patch_buttons=load_list_arg(args.patch_buttons_file, option_name="--patch-buttons-file"),
|
|
369
374
|
remove_buttons=load_list_arg(args.remove_buttons_file, option_name="--remove-buttons-file"),
|
|
370
375
|
view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
|
|
371
376
|
)
|
|
@@ -376,6 +381,7 @@ def _handle_associated_resource_apply(args: argparse.Namespace, context: CliCont
|
|
|
376
381
|
profile=args.profile,
|
|
377
382
|
app_key=args.app_key,
|
|
378
383
|
upsert_resources=load_list_arg(args.upsert_resources_file, option_name="--upsert-resources-file"),
|
|
384
|
+
patch_resources=load_list_arg(args.patch_resources_file, option_name="--patch-resources-file"),
|
|
379
385
|
remove_associated_item_ids=load_list_arg(args.remove_associated_item_ids_file, option_name="--remove-associated-item-ids-file"),
|
|
380
386
|
reorder_associated_item_ids=load_list_arg(args.reorder_associated_item_ids_file, option_name="--reorder-associated-item-ids-file"),
|
|
381
387
|
view_configs=load_list_arg(args.view_configs_file, option_name="--view-configs-file"),
|
|
@@ -494,6 +500,7 @@ def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
494
500
|
app_key=args.app_key,
|
|
495
501
|
publish=bool(args.publish),
|
|
496
502
|
upsert_views=load_list_arg(args.upsert_views_file, option_name="--upsert-views-file"),
|
|
503
|
+
patch_views=load_list_arg(args.patch_views_file, option_name="--patch-views-file"),
|
|
497
504
|
remove_views=load_list_arg(args.remove_views_file, option_name="--remove-views-file"),
|
|
498
505
|
)
|
|
499
506
|
|
|
@@ -514,6 +521,7 @@ def _handle_charts_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
514
521
|
profile=args.profile,
|
|
515
522
|
app_key=args.app_key,
|
|
516
523
|
upsert_charts=load_list_arg(args.upsert_file, option_name="--upsert-file"),
|
|
524
|
+
patch_charts=load_list_arg(args.patch_file, option_name="--patch-file"),
|
|
517
525
|
remove_chart_ids=load_list_arg(args.remove_chart_ids_file, option_name="--remove-chart-ids-file"),
|
|
518
526
|
reorder_chart_ids=load_list_arg(args.reorder_chart_ids_file, option_name="--reorder-chart-ids-file"),
|
|
519
527
|
)
|
|
@@ -13,7 +13,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
13
13
|
record_subparsers = parser.add_subparsers(
|
|
14
14
|
dest="record_command",
|
|
15
15
|
required=True,
|
|
16
|
-
metavar="{schema,list,access,get,insert,update,delete,code-block-run}",
|
|
16
|
+
metavar="{schema,list,access,get,insert,update,delete,member-candidates,department-candidates,code-block-run}",
|
|
17
17
|
)
|
|
18
18
|
|
|
19
19
|
schema = record_subparsers.add_parser("schema", help="读取记录相关表结构")
|
|
@@ -47,6 +47,28 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
47
47
|
schema_code_block.add_argument("--app-key", required=True)
|
|
48
48
|
schema_code_block.set_defaults(handler=_handle_schema_code_block, format_hint="")
|
|
49
49
|
|
|
50
|
+
member_candidates = record_subparsers.add_parser("member-candidates", help="读取成员字段候选项")
|
|
51
|
+
member_candidates.add_argument("--app-key", required=True)
|
|
52
|
+
member_candidates.add_argument("--field-id", type=int, required=True)
|
|
53
|
+
member_candidates.add_argument("--keyword", default="")
|
|
54
|
+
member_candidates.add_argument("--page-num", type=int, default=1)
|
|
55
|
+
member_candidates.add_argument("--page-size", type=int, default=20)
|
|
56
|
+
member_candidates.add_argument("--record-id")
|
|
57
|
+
member_candidates.add_argument("--workflow-node-id", type=int)
|
|
58
|
+
member_candidates.add_argument("--fields-file")
|
|
59
|
+
member_candidates.set_defaults(handler=_handle_member_candidates, format_hint="")
|
|
60
|
+
|
|
61
|
+
department_candidates = record_subparsers.add_parser("department-candidates", help="读取部门字段候选项")
|
|
62
|
+
department_candidates.add_argument("--app-key", required=True)
|
|
63
|
+
department_candidates.add_argument("--field-id", type=int, required=True)
|
|
64
|
+
department_candidates.add_argument("--keyword", default="")
|
|
65
|
+
department_candidates.add_argument("--page-num", type=int, default=1)
|
|
66
|
+
department_candidates.add_argument("--page-size", type=int, default=20)
|
|
67
|
+
department_candidates.add_argument("--record-id")
|
|
68
|
+
department_candidates.add_argument("--workflow-node-id", type=int)
|
|
69
|
+
department_candidates.add_argument("--fields-file")
|
|
70
|
+
department_candidates.set_defaults(handler=_handle_department_candidates, format_hint="")
|
|
71
|
+
|
|
50
72
|
list_parser = record_subparsers.add_parser("list", help="列出记录")
|
|
51
73
|
list_parser.add_argument("--app-key", required=True)
|
|
52
74
|
list_parser.add_argument("--column", dest="columns", action="append", type=int, default=[])
|
|
@@ -207,6 +229,38 @@ def _handle_schema_code_block(args: argparse.Namespace, context: CliContext) ->
|
|
|
207
229
|
return context.code_block.record_code_block_schema_get_public(profile=args.profile, app_key=args.app_key)
|
|
208
230
|
|
|
209
231
|
|
|
232
|
+
def _candidate_context_fields(args: argparse.Namespace) -> dict[str, Any]:
|
|
233
|
+
return load_object_arg(args.fields_file, option_name="--fields-file") or {}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _handle_member_candidates(args: argparse.Namespace, context: CliContext) -> dict:
|
|
237
|
+
return context.record.record_member_candidates(
|
|
238
|
+
profile=args.profile,
|
|
239
|
+
app_key=args.app_key,
|
|
240
|
+
field_id=args.field_id,
|
|
241
|
+
record_id=args.record_id,
|
|
242
|
+
workflow_node_id=args.workflow_node_id,
|
|
243
|
+
fields=_candidate_context_fields(args),
|
|
244
|
+
keyword=args.keyword,
|
|
245
|
+
page_num=args.page_num,
|
|
246
|
+
page_size=args.page_size,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _handle_department_candidates(args: argparse.Namespace, context: CliContext) -> dict:
|
|
251
|
+
return context.record.record_department_candidates(
|
|
252
|
+
profile=args.profile,
|
|
253
|
+
app_key=args.app_key,
|
|
254
|
+
field_id=args.field_id,
|
|
255
|
+
record_id=args.record_id,
|
|
256
|
+
workflow_node_id=args.workflow_node_id,
|
|
257
|
+
fields=_candidate_context_fields(args),
|
|
258
|
+
keyword=args.keyword,
|
|
259
|
+
page_num=args.page_num,
|
|
260
|
+
page_size=args.page_size,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
210
264
|
def _validate_public_view_selector(
|
|
211
265
|
*,
|
|
212
266
|
view_id: str | None,
|
|
@@ -78,8 +78,8 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
78
78
|
("record_code_block_schema_get_public",),
|
|
79
79
|
("record", "schema", "code-block"),
|
|
80
80
|
),
|
|
81
|
-
PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",),
|
|
82
|
-
PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",),
|
|
81
|
+
PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), ("record", "member-candidates")),
|
|
82
|
+
PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), ("record", "department-candidates")),
|
|
83
83
|
PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False),
|
|
84
84
|
PublicToolSpec(USER_DOMAIN, "record_list", ("record_list",), ("record", "list"), cli_show_effective_context=True),
|
|
85
85
|
PublicToolSpec(USER_DOMAIN, "record_access", ("record_access",), ("record", "access"), cli_show_effective_context=True),
|
|
@@ -91,7 +91,12 @@ def trim_error_response(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
91
91
|
_drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
|
|
92
92
|
details = trimmed.get("details")
|
|
93
93
|
if isinstance(details, dict):
|
|
94
|
+
preserved = {}
|
|
95
|
+
for key in ("blocking_issues", "compiled_match_rules"):
|
|
96
|
+
if key in details:
|
|
97
|
+
preserved[key] = details.get(key)
|
|
94
98
|
compact_details = _compact_scalar_dict(details)
|
|
99
|
+
compact_details.update(preserved)
|
|
95
100
|
if compact_details:
|
|
96
101
|
trimmed["details"] = compact_details
|
|
97
102
|
else:
|
|
@@ -156,7 +161,12 @@ def _trim_returned_failure(payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
156
161
|
_drop_deep_keys(trimmed.get("details"), {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
|
|
157
162
|
details = trimmed.get("details")
|
|
158
163
|
if isinstance(details, dict):
|
|
164
|
+
preserved = {}
|
|
165
|
+
for key in ("blocking_issues", "compiled_match_rules"):
|
|
166
|
+
if key in details:
|
|
167
|
+
preserved[key] = details.get(key)
|
|
159
168
|
compact_details = _compact_scalar_dict(details)
|
|
169
|
+
compact_details.update(preserved)
|
|
160
170
|
if compact_details:
|
|
161
171
|
trimmed["details"] = compact_details
|
|
162
172
|
else:
|
|
@@ -944,7 +954,11 @@ def _trim_builder_envelope(payload: JSONObject) -> None:
|
|
|
944
954
|
details = payload.get("details")
|
|
945
955
|
if isinstance(details, dict):
|
|
946
956
|
_drop_deep_keys(details, {"request_route", "base_url", "normalized_args", "suggested_next_call", "transport", "response", "body", "raw"})
|
|
957
|
+
preserved = {}
|
|
958
|
+
if isinstance(details.get("compiled_match_rules"), dict):
|
|
959
|
+
preserved["compiled_match_rules"] = details.get("compiled_match_rules")
|
|
947
960
|
compact = _compact_scalar_dict(details)
|
|
961
|
+
compact.update(preserved)
|
|
948
962
|
if compact:
|
|
949
963
|
payload["details"] = compact
|
|
950
964
|
else:
|
|
@@ -131,6 +131,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
131
131
|
|
|
132
132
|
- Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
|
|
133
133
|
- Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
|
|
134
|
+
- CLI-only agents can use `qingflow record member-candidates --app-key APP_KEY --field-id FIELD_ID --keyword NAME --json` or `qingflow record department-candidates --app-key APP_KEY --field-id FIELD_ID --keyword DEPT --json` for that fallback; pass `--record-id`, `--workflow-node-id`, and `--fields-file` only when the candidate scope must match an existing runtime context.
|
|
134
135
|
- For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
|
|
135
136
|
- If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
|
|
136
137
|
|
|
@@ -39,10 +39,13 @@ 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
|
+
"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. "
|
|
42
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. "
|
|
43
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 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. 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. "
|
|
45
|
-
"For
|
|
45
|
+
"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
|
+
"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
|
+
"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. "
|
|
48
|
+
"For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
|
|
46
49
|
"For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
|
|
47
50
|
"Use package_apply to manage package metadata, visibility, grouping, and ordering, and app_publish_verify for explicit final publish verification. "
|
|
48
51
|
"For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
|
|
@@ -303,6 +306,7 @@ def build_builder_server() -> FastMCP:
|
|
|
303
306
|
profile: str = DEFAULT_PROFILE,
|
|
304
307
|
app_key: str = "",
|
|
305
308
|
upsert_buttons: list[dict] | None = None,
|
|
309
|
+
patch_buttons: list[dict] | None = None,
|
|
306
310
|
remove_buttons: list[dict] | None = None,
|
|
307
311
|
view_configs: list[dict] | None = None,
|
|
308
312
|
) -> dict:
|
|
@@ -310,6 +314,7 @@ def build_builder_server() -> FastMCP:
|
|
|
310
314
|
profile=profile,
|
|
311
315
|
app_key=app_key,
|
|
312
316
|
upsert_buttons=upsert_buttons or [],
|
|
317
|
+
patch_buttons=patch_buttons or [],
|
|
313
318
|
remove_buttons=remove_buttons or [],
|
|
314
319
|
view_configs=view_configs or [],
|
|
315
320
|
)
|
|
@@ -319,6 +324,7 @@ def build_builder_server() -> FastMCP:
|
|
|
319
324
|
profile: str = DEFAULT_PROFILE,
|
|
320
325
|
app_key: str = "",
|
|
321
326
|
upsert_resources: list[dict] | None = None,
|
|
327
|
+
patch_resources: list[dict] | None = None,
|
|
322
328
|
remove_associated_item_ids: list[int] | None = None,
|
|
323
329
|
reorder_associated_item_ids: list[int] | None = None,
|
|
324
330
|
view_configs: list[dict] | None = None,
|
|
@@ -327,6 +333,7 @@ def build_builder_server() -> FastMCP:
|
|
|
327
333
|
profile=profile,
|
|
328
334
|
app_key=app_key,
|
|
329
335
|
upsert_resources=upsert_resources or [],
|
|
336
|
+
patch_resources=patch_resources or [],
|
|
330
337
|
remove_associated_item_ids=remove_associated_item_ids or [],
|
|
331
338
|
reorder_associated_item_ids=reorder_associated_item_ids or [],
|
|
332
339
|
view_configs=view_configs or [],
|
|
@@ -466,6 +473,7 @@ def build_builder_server() -> FastMCP:
|
|
|
466
473
|
app_key: str = "",
|
|
467
474
|
publish: bool = True,
|
|
468
475
|
upsert_views: list[dict] | None = None,
|
|
476
|
+
patch_views: list[dict] | None = None,
|
|
469
477
|
remove_views: list[str] | None = None,
|
|
470
478
|
) -> dict:
|
|
471
479
|
return ai_builder.app_views_apply(
|
|
@@ -473,6 +481,7 @@ def build_builder_server() -> FastMCP:
|
|
|
473
481
|
app_key=app_key,
|
|
474
482
|
publish=publish,
|
|
475
483
|
upsert_views=upsert_views or [],
|
|
484
|
+
patch_views=patch_views or [],
|
|
476
485
|
remove_views=remove_views or [],
|
|
477
486
|
)
|
|
478
487
|
|
|
@@ -481,6 +490,7 @@ def build_builder_server() -> FastMCP:
|
|
|
481
490
|
profile: str = DEFAULT_PROFILE,
|
|
482
491
|
app_key: str = "",
|
|
483
492
|
upsert_charts: list[dict] | None = None,
|
|
493
|
+
patch_charts: list[dict] | None = None,
|
|
484
494
|
remove_chart_ids: list[str] | None = None,
|
|
485
495
|
reorder_chart_ids: list[str] | None = None,
|
|
486
496
|
) -> dict:
|
|
@@ -488,6 +498,7 @@ def build_builder_server() -> FastMCP:
|
|
|
488
498
|
profile=profile,
|
|
489
499
|
app_key=app_key,
|
|
490
500
|
upsert_charts=upsert_charts or [],
|
|
501
|
+
patch_charts=patch_charts or [],
|
|
491
502
|
remove_chart_ids=remove_chart_ids or [],
|
|
492
503
|
reorder_chart_ids=reorder_chart_ids or [],
|
|
493
504
|
)
|
|
@@ -132,6 +132,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
132
132
|
|
|
133
133
|
- Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
|
|
134
134
|
- Member, department, and relation fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to candidate tools when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
|
|
135
|
+
- CLI-only agents can use `qingflow record member-candidates --app-key APP_KEY --field-id FIELD_ID --keyword NAME --json` or `qingflow record department-candidates --app-key APP_KEY --field-id FIELD_ID --keyword DEPT --json` for that fallback.
|
|
135
136
|
- For batch insert `partial_success`, read `created_record_ids`, failed `items[].row_number`, and `failed_fields`; repair only failed rows and never retry the whole batch after any row has `write_executed=true`.
|
|
136
137
|
- When candidate browsing must match a real update/write scope, pass `record_id`, `workflow_node_id`, and any pending `fields` context to the candidate tool; otherwise the candidate result is only a static applicant-node preview.
|
|
137
138
|
- If explicit candidate browsing is needed for default-all member or department fields, prefer those field candidate tools instead of starting with `directory_*`.
|