@qingflow-tech/qingflow-cli 1.1.50-beta.2 → 1.1.50-beta.3
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 +7 -2
- package/skills/qingflow-cli/SKILL.md +74 -0
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +1 -0
- package/skills/qingflow-cli/reference/builder/50-views.md +176 -190
- package/skills/qingflow-cli/reference/builder/README.md +1 -1
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +3 -3
- package/skills/qingflow-cli/reference/examples/README.md +1 -1
- package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
- package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
- package/src/qingflow_mcp/builder_facade/models.py +208 -17
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +43 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1193 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +71 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +274 -0
- package/src/qingflow_mcp/builder_facade/service.py +1427 -410
- package/src/qingflow_mcp/builder_facade/views_extract.py +572 -0
- package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
- package/src/qingflow_mcp/cli/commands/builder.py +72 -53
- package/src/qingflow_mcp/public_surface.py +3 -0
- package/src/qingflow_mcp/response_trim.py +11 -2
- package/src/qingflow_mcp/server_app_builder.py +32 -8
- package/src/qingflow_mcp/solution/executor.py +93 -7
- package/src/qingflow_mcp/tools/ai_builder_tools.py +211 -393
- package/src/qingflow_mcp/tools/solution_tools.py +2 -0
- package/src/qingflow_mcp/tools/view_tools.py +0 -6
- package/src/qingflow_mcp/tools/view_writer.py +44 -0
- package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
|
@@ -56,8 +56,8 @@ def build_builder_server() -> FastMCP:
|
|
|
56
56
|
"For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
|
|
57
57
|
"For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
|
|
58
58
|
"For app_form_apply, pin the version from app_form_schema, preserve the complete body and existing IDs from a draft get, and configure data display through app-level spec.dataTitleField/spec.dataCoverField selectors rather than booleans on fields; App Builder MCP validates the declaration inside app_form_apply, while the CLI route has a separate static validate command. dataTitleField must resolve to exactly one top-level field for a non-empty body, while dataCoverField is optional and must resolve to a top-level attachment field. A root appKey means update and may include packageId; no appKey means create and requires packageId plus spec.appName. AppForm does not accept spec.icon or spec.color; creation derives a stable default icon and color from spec.appName. "
|
|
59
|
-
"For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows
|
|
60
|
-
"For ordinary business buttons, declare them on
|
|
59
|
+
"For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows accepts field names, field IDs, or queIds and compiles them to backend queryCondition queIds, with queIds preferred. 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. View writes use ViewManagementAuth (beingViewManageStatus), falling back to DataManageAuth when advanced app permissions are not enabled. "
|
|
60
|
+
"For ordinary business buttons, declare them on a new or full view with app_views_apply.upsert_view.action_buttons; app_views_apply writes the view first, then creates/binds the button. For an existing view, use patch_views[].set.action_buttons. Use app_custom_buttons_apply only when the user explicitly asks to maintain existing standalone buttons outside view design, such as custom style/icon changes, cross-view reuse, deleting button bodies, bulk binding reorder, or exact qRobot/wings config. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. For addData buttons, prefer action_buttons.target_app_key + field_mappings/default_values on the view path; 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; standalone view_configs are only for explicit maintenance. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
|
|
61
61
|
"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; chart dimension/metric/filter/query fields must come from app_get_fields.chart_fields, not record schema or form-only fields; 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. "
|
|
62
62
|
"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. "
|
|
63
63
|
"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. "
|
|
@@ -231,6 +231,32 @@ def build_builder_server() -> FastMCP:
|
|
|
231
231
|
) -> dict:
|
|
232
232
|
return ai_builder.app_form_apply(profile=profile, spec=spec or {})
|
|
233
233
|
|
|
234
|
+
@server.tool()
|
|
235
|
+
def app_views_schema(
|
|
236
|
+
schema_version: str | None = None,
|
|
237
|
+
schema_kind: str | None = None,
|
|
238
|
+
) -> dict:
|
|
239
|
+
"""Get a complete views schema or a focused detail schema such as display or button-config."""
|
|
240
|
+
return ai_builder.app_views_schema(schema_version=schema_version, schema_kind=schema_kind)
|
|
241
|
+
|
|
242
|
+
@server.tool()
|
|
243
|
+
def app_views_get(
|
|
244
|
+
profile: str = DEFAULT_PROFILE,
|
|
245
|
+
app_key: str = "",
|
|
246
|
+
schema_version: str | None = None,
|
|
247
|
+
schema_kind: str | None = None,
|
|
248
|
+
) -> dict:
|
|
249
|
+
return ai_builder.app_views_get(
|
|
250
|
+
profile=profile, app_key=app_key, schema_version=schema_version, schema_kind=schema_kind
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@server.tool()
|
|
254
|
+
def app_views_validate(
|
|
255
|
+
declaration: dict | None = None,
|
|
256
|
+
schema_version: str | None = None,
|
|
257
|
+
) -> dict:
|
|
258
|
+
return ai_builder.app_views_validate(declaration=declaration or {}, schema_version=schema_version)
|
|
259
|
+
|
|
234
260
|
@server.tool(description=ai_builder._high_risk_tool_description(operation="delete", target="app configuration"))
|
|
235
261
|
def app_delete(
|
|
236
262
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -546,21 +572,19 @@ def build_builder_server() -> FastMCP:
|
|
|
546
572
|
profile: str = DEFAULT_PROFILE,
|
|
547
573
|
app_key: str = "",
|
|
548
574
|
publish: bool = True,
|
|
549
|
-
|
|
575
|
+
upsert_view: dict | None = None,
|
|
550
576
|
patch_views: list[dict] | None = None,
|
|
551
577
|
remove_views: list[str] | None = None,
|
|
552
|
-
|
|
553
|
-
views: list[dict] | None = None,
|
|
578
|
+
schema_version: str | None = None,
|
|
554
579
|
) -> dict:
|
|
555
580
|
return ai_builder.app_views_apply(
|
|
556
581
|
profile=profile,
|
|
557
582
|
app_key=app_key,
|
|
558
583
|
publish=publish,
|
|
559
|
-
|
|
584
|
+
upsert_view=upsert_view,
|
|
560
585
|
patch_views=patch_views or [],
|
|
561
586
|
remove_views=remove_views or [],
|
|
562
|
-
|
|
563
|
-
views=views,
|
|
587
|
+
schema_version=schema_version,
|
|
564
588
|
)
|
|
565
589
|
|
|
566
590
|
@server.tool()
|
|
@@ -36,6 +36,7 @@ class SolutionExecutor:
|
|
|
36
36
|
app_tools: AppTools,
|
|
37
37
|
record_tools: RecordTools,
|
|
38
38
|
view_tools: ViewTools,
|
|
39
|
+
view_writer: "ViewWriter | None" = None,
|
|
39
40
|
chart_tools: QingbiReportTools,
|
|
40
41
|
portal_tools: PortalTools,
|
|
41
42
|
navigation_tools: NavigationTools,
|
|
@@ -47,6 +48,7 @@ class SolutionExecutor:
|
|
|
47
48
|
self.app_tools = app_tools
|
|
48
49
|
self.record_tools = record_tools
|
|
49
50
|
self.view_tools = view_tools
|
|
51
|
+
self.view_writer = view_writer
|
|
50
52
|
self.chart_tools = chart_tools
|
|
51
53
|
self.portal_tools = portal_tools
|
|
52
54
|
self.navigation_tools = navigation_tools
|
|
@@ -460,6 +462,20 @@ class SolutionExecutor:
|
|
|
460
462
|
"Pass {app_key, spec} to solution_build_flow or use qingflow builder flow apply."
|
|
461
463
|
)
|
|
462
464
|
|
|
465
|
+
def _create_view_via_writer(self, *, profile: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
466
|
+
"""视图创建唯一落点:共享 ViewWriter(POST /view/viewConfig)。
|
|
467
|
+
|
|
468
|
+
solution DSL 内部创建路径不再直接调用 ViewTools.view_create(该方法已删除);
|
|
469
|
+
未注入 writer 的旧调用方退化为直接经 ViewTools 同一会话执行同一端点。
|
|
470
|
+
"""
|
|
471
|
+
if self.view_writer is not None:
|
|
472
|
+
return self.view_writer.create_view(profile=profile, payload=payload)
|
|
473
|
+
from ..tools.view_writer import ViewWriter as _FallbackViewWriter
|
|
474
|
+
|
|
475
|
+
return _FallbackViewWriter(self.view_tools.sessions, self.view_tools.backend).create_view(
|
|
476
|
+
profile=profile, payload=payload
|
|
477
|
+
)
|
|
478
|
+
|
|
463
479
|
def _build_views(self, profile: str, entity: CompiledEntity, store: RunArtifactStore) -> None:
|
|
464
480
|
app_key = self._get_app_key(store, entity.entity_id)
|
|
465
481
|
field_meta = store.get_artifact("field_maps", entity.entity_id, {})
|
|
@@ -474,7 +490,7 @@ class SolutionExecutor:
|
|
|
474
490
|
schema=schema,
|
|
475
491
|
group_by_field_id=view_plan["group_by_field_id"],
|
|
476
492
|
)
|
|
477
|
-
result = self.
|
|
493
|
+
result = self._create_view_via_writer(profile=profile, payload=create_payload)
|
|
478
494
|
view_result = result.get("result")
|
|
479
495
|
view_key = ((view_result or {}) if isinstance(view_result, dict) else {}).get("viewgraphKey")
|
|
480
496
|
if not view_key and isinstance(view_result, str):
|
|
@@ -2324,36 +2340,106 @@ def _resolve_package_item_insert_index(tag_items: list[dict[str, Any]], ordinal:
|
|
|
2324
2340
|
return max(0, min(len(tag_items), ordinal - 1))
|
|
2325
2341
|
|
|
2326
2342
|
|
|
2327
|
-
def
|
|
2343
|
+
def _flatten_viewgraph_questions(entries: list[Any] | None) -> dict[int, dict[str, Any]]:
|
|
2344
|
+
"""把读面的 viewgraphQuestions(含 subQues/innerQues 嵌套)按 queId 展平。"""
|
|
2345
|
+
flat: dict[int, dict[str, Any]] = {}
|
|
2346
|
+
|
|
2347
|
+
def walk(entry: Any) -> None:
|
|
2348
|
+
if not isinstance(entry, dict):
|
|
2349
|
+
return
|
|
2350
|
+
que_id = entry.get("queId")
|
|
2351
|
+
if isinstance(que_id, int) and que_id not in flat:
|
|
2352
|
+
flat[que_id] = entry
|
|
2353
|
+
for key in ("subQues", "innerQues"):
|
|
2354
|
+
for child in entry.get(key) or []:
|
|
2355
|
+
if isinstance(child, list):
|
|
2356
|
+
for inner in child:
|
|
2357
|
+
walk(inner)
|
|
2358
|
+
else:
|
|
2359
|
+
walk(child)
|
|
2360
|
+
|
|
2361
|
+
for entry in entries or []:
|
|
2362
|
+
if isinstance(entry, list):
|
|
2363
|
+
for inner in entry:
|
|
2364
|
+
walk(inner)
|
|
2365
|
+
else:
|
|
2366
|
+
walk(entry)
|
|
2367
|
+
return flat
|
|
2368
|
+
|
|
2369
|
+
|
|
2370
|
+
def _build_viewgraph_questions(
|
|
2371
|
+
schema: dict[str, Any],
|
|
2372
|
+
visible_que_ids: list[int],
|
|
2373
|
+
*,
|
|
2374
|
+
existing_by_que_id: dict[int, dict[str, Any]] | None = None,
|
|
2375
|
+
) -> list[dict[str, Any]]:
|
|
2376
|
+
"""重建业务列行;更新时原样保留已配置的系统列。
|
|
2377
|
+
|
|
2378
|
+
existing_by_que_id(读面展平行)存在时,以读面行为基底只更新可见性/序号——
|
|
2379
|
+
保留列级权限(queAuth/beingAddRow/beingDeleteRow/beingEditRow/beingDownload 等),
|
|
2380
|
+
否则重建会把这些标志归默认值,破坏既有列权限配置。
|
|
2381
|
+
系统列读面占位对象不写回;existing_by_que_id=None 表示新建。
|
|
2382
|
+
"""
|
|
2328
2383
|
visible_map = {que_id: index for index, que_id in enumerate(visible_que_ids, start=1)}
|
|
2384
|
+
existing = existing_by_que_id or {}
|
|
2329
2385
|
questions: list[dict[str, Any]] = []
|
|
2330
2386
|
for base_question in schema.get("baseQues", []):
|
|
2331
|
-
question = _build_viewgraph_question(base_question, visible_map, default_auth=3)
|
|
2387
|
+
question = _build_viewgraph_question(base_question, visible_map, default_auth=3, existing=existing_by_que_id)
|
|
2332
2388
|
if question is not None:
|
|
2333
2389
|
questions.append(question)
|
|
2334
2390
|
for line in schema.get("formQues", []):
|
|
2335
2391
|
for question in line:
|
|
2336
|
-
resolved = _build_viewgraph_question(question, visible_map, default_auth=1)
|
|
2392
|
+
resolved = _build_viewgraph_question(question, visible_map, default_auth=1, existing=existing_by_que_id)
|
|
2337
2393
|
if resolved is not None:
|
|
2338
2394
|
questions.append(resolved)
|
|
2395
|
+
# 某些系统列只在视图读面出现,不属于表单 schema。
|
|
2396
|
+
emitted_que_ids = _flatten_viewgraph_questions(questions)
|
|
2397
|
+
for que_id, prior in existing.items():
|
|
2398
|
+
if que_id <= 5 and que_id not in emitted_que_ids:
|
|
2399
|
+
preserved = _build_viewgraph_question(prior, visible_map, default_auth=3, existing=existing)
|
|
2400
|
+
if preserved is not None:
|
|
2401
|
+
questions.append(preserved)
|
|
2339
2402
|
return questions
|
|
2340
2403
|
|
|
2341
2404
|
|
|
2342
|
-
def _build_viewgraph_question(
|
|
2405
|
+
def _build_viewgraph_question(
|
|
2406
|
+
question: dict[str, Any],
|
|
2407
|
+
visible_map: dict[int, int],
|
|
2408
|
+
*,
|
|
2409
|
+
default_auth: int,
|
|
2410
|
+
existing: dict[int, dict[str, Any]] | None = None,
|
|
2411
|
+
) -> dict[str, Any] | None:
|
|
2343
2412
|
que_id = question.get("queId")
|
|
2344
2413
|
if que_id is None:
|
|
2345
2414
|
return None
|
|
2415
|
+
if isinstance(que_id, int) and que_id <= 5 and existing is not None:
|
|
2416
|
+
prior = existing.get(que_id)
|
|
2417
|
+
# 后端会生成未落库的系统字段占位对象;只有落库配置才带这些列状态。
|
|
2418
|
+
if prior is None or all(prior.get(key) is None for key in (
|
|
2419
|
+
"beingListDisplay", "displayOrdinal", "beingAddRow", "beingEditRow", "beingDeleteRow",
|
|
2420
|
+
)):
|
|
2421
|
+
return None
|
|
2422
|
+
return deepcopy(prior)
|
|
2346
2423
|
sub_questions = [
|
|
2347
2424
|
resolved
|
|
2348
2425
|
for sub_question in question.get("subQuestions", []) or []
|
|
2349
|
-
if (resolved := _build_viewgraph_question(sub_question, visible_map, default_auth=1)) is not None
|
|
2426
|
+
if (resolved := _build_viewgraph_question(sub_question, visible_map, default_auth=1, existing=existing)) is not None
|
|
2350
2427
|
]
|
|
2351
2428
|
inner_questions = [
|
|
2352
2429
|
resolved
|
|
2353
2430
|
for inner_row in question.get("innerQuestions", []) or []
|
|
2354
2431
|
for inner_question in inner_row or []
|
|
2355
|
-
if (resolved := _build_viewgraph_question(inner_question, visible_map, default_auth=1)) is not None
|
|
2432
|
+
if (resolved := _build_viewgraph_question(inner_question, visible_map, default_auth=1, existing=existing)) is not None
|
|
2356
2433
|
]
|
|
2434
|
+
prior = (existing or {}).get(que_id) if isinstance(que_id, int) else None
|
|
2435
|
+
if prior is not None:
|
|
2436
|
+
# 读面行为基底:保留全部列级标志,只更新可见性/序号;嵌套子项同样融合
|
|
2437
|
+
merged = deepcopy(prior)
|
|
2438
|
+
merged["beingListDisplay"] = que_id in visible_map
|
|
2439
|
+
merged["displayOrdinal"] = visible_map.get(que_id, -1)
|
|
2440
|
+
merged["subQues"] = sub_questions
|
|
2441
|
+
merged["innerQues"] = inner_questions
|
|
2442
|
+
return merged
|
|
2357
2443
|
return {
|
|
2358
2444
|
"queId": que_id,
|
|
2359
2445
|
"queTitle": question.get("queTitle"),
|