@josephyan/qingflow-app-builder-mcp 0.2.0-beta.37 → 0.2.0-beta.39
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 +25 -30
- package/skills/qingflow-app-builder/references/create-app.md +7 -27
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +4 -5
- package/skills/qingflow-app-builder/references/gotchas.md +3 -3
- package/skills/qingflow-app-builder/references/solution-playbooks.md +8 -13
- package/skills/qingflow-app-builder/references/tool-selection.md +15 -20
- package/skills/qingflow-app-builder/references/update-flow.md +11 -45
- package/skills/qingflow-app-builder/references/update-layout.md +3 -26
- package/skills/qingflow-app-builder/references/update-schema.md +1 -23
- package/skills/qingflow-app-builder/references/update-views.md +6 -28
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/models.py +263 -0
- package/src/qingflow_mcp/builder_facade/service.py +1075 -46
- package/src/qingflow_mcp/server_app_builder.py +48 -75
- package/src/qingflow_mcp/tools/ai_builder_tools.py +528 -165
- package/src/qingflow_mcp/tools/portal_tools.py +31 -0
|
@@ -4,12 +4,11 @@ Use this when the task is only about table, card, board, or gantt views.
|
|
|
4
4
|
|
|
5
5
|
## Minimal sequence
|
|
6
6
|
|
|
7
|
-
1. `builder_tool_contract(tool_name="
|
|
7
|
+
1. `builder_tool_contract(tool_name="app_views_apply")`
|
|
8
8
|
2. `app_read_fields`
|
|
9
9
|
3. `app_read_views_summary`
|
|
10
|
-
4. `
|
|
11
|
-
5. `
|
|
12
|
-
6. `app_read_views_summary` again whenever apply returns `failed` or `partial_success`
|
|
10
|
+
4. `app_views_apply`
|
|
11
|
+
5. `app_read_views_summary` again whenever apply returns `failed` or `partial_success`
|
|
13
12
|
|
|
14
13
|
If you are unsure about keys or view types, call `builder_tool_contract(tool_name="app_views_apply")` before guessing.
|
|
15
14
|
|
|
@@ -30,28 +29,7 @@ Canonical rules before any example:
|
|
|
30
29
|
- For gantt, use `start_field`, `end_field`, and optionally `title_field`
|
|
31
30
|
- If `app_read_views_summary` shows duplicate view names, include `view_key` in `upsert_views[]` and update that exact target
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
```json
|
|
36
|
-
{
|
|
37
|
-
"tool_name": "app_views_plan",
|
|
38
|
-
"arguments": {
|
|
39
|
-
"profile": "default",
|
|
40
|
-
"app_key": "APP_123",
|
|
41
|
-
"upsert_views": [
|
|
42
|
-
{
|
|
43
|
-
"name": "全部订单",
|
|
44
|
-
"view_key": "VIEW_KEY_IF_DUPLICATE_NAMES_EXIST",
|
|
45
|
-
"type": "table",
|
|
46
|
-
"columns": ["订单编号", "客户名称", "订单金额", "状态"]
|
|
47
|
-
}
|
|
48
|
-
],
|
|
49
|
-
"remove_views": []
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Apply it:
|
|
32
|
+
Apply a default table view:
|
|
55
33
|
|
|
56
34
|
```json
|
|
57
35
|
{
|
|
@@ -63,6 +41,7 @@ Apply it:
|
|
|
63
41
|
"upsert_views": [
|
|
64
42
|
{
|
|
65
43
|
"name": "全部订单",
|
|
44
|
+
"view_key": "VIEW_KEY_IF_DUPLICATE_NAMES_EXIST",
|
|
66
45
|
"type": "table",
|
|
67
46
|
"columns": ["订单编号", "客户名称", "订单金额", "状态"]
|
|
68
47
|
}
|
|
@@ -71,8 +50,7 @@ Apply it:
|
|
|
71
50
|
}
|
|
72
51
|
}
|
|
73
52
|
```
|
|
74
|
-
|
|
75
|
-
After `app_views_plan` succeeds, prefer reusing its `suggested_next_call.arguments` directly. Do not rewrite aliases back into non-canonical keys such as `column_names`.
|
|
53
|
+
After `app_views_apply` returns canonical arguments or blocking issues, prefer reusing its `suggested_next_call.arguments` directly. Do not rewrite aliases back into non-canonical keys such as `column_names`.
|
|
76
54
|
|
|
77
55
|
Board example:
|
|
78
56
|
|
|
@@ -69,6 +69,14 @@ class PublicViewType(str, Enum):
|
|
|
69
69
|
gantt = "gantt"
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
class PublicChartType(str, Enum):
|
|
73
|
+
target = "target"
|
|
74
|
+
pie = "pie"
|
|
75
|
+
bar = "bar"
|
|
76
|
+
line = "line"
|
|
77
|
+
table = "table"
|
|
78
|
+
|
|
79
|
+
|
|
72
80
|
class LayoutApplyMode(str, Enum):
|
|
73
81
|
merge = "merge"
|
|
74
82
|
replace = "replace"
|
|
@@ -491,6 +499,261 @@ class ViewUpsertPatch(StrictModel):
|
|
|
491
499
|
return self
|
|
492
500
|
|
|
493
501
|
|
|
502
|
+
class ChartFilterRulePatch(StrictModel):
|
|
503
|
+
field_name: str = Field(validation_alias=AliasChoices("field_name", "fieldName", "field", "name"))
|
|
504
|
+
operator: ViewFilterOperator = Field(validation_alias=AliasChoices("operator", "op"))
|
|
505
|
+
values: list[Any] = Field(default_factory=list)
|
|
506
|
+
|
|
507
|
+
@model_validator(mode="before")
|
|
508
|
+
@classmethod
|
|
509
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
510
|
+
if not isinstance(value, dict):
|
|
511
|
+
return value
|
|
512
|
+
payload = dict(value)
|
|
513
|
+
if "value" in payload and "values" not in payload:
|
|
514
|
+
payload["values"] = [payload.pop("value")]
|
|
515
|
+
raw_operator = payload.get("operator", payload.get("op"))
|
|
516
|
+
if isinstance(raw_operator, str):
|
|
517
|
+
normalized = raw_operator.strip().lower()
|
|
518
|
+
operator_aliases = {
|
|
519
|
+
"equals": ViewFilterOperator.eq.value,
|
|
520
|
+
"equal": ViewFilterOperator.eq.value,
|
|
521
|
+
"=": ViewFilterOperator.eq.value,
|
|
522
|
+
"not_equals": ViewFilterOperator.neq.value,
|
|
523
|
+
"not_equal": ViewFilterOperator.neq.value,
|
|
524
|
+
"!=": ViewFilterOperator.neq.value,
|
|
525
|
+
">=": ViewFilterOperator.gte.value,
|
|
526
|
+
"<=": ViewFilterOperator.lte.value,
|
|
527
|
+
"any_of": ViewFilterOperator.in_.value,
|
|
528
|
+
"one_of": ViewFilterOperator.in_.value,
|
|
529
|
+
"between_any": ViewFilterOperator.in_.value,
|
|
530
|
+
"empty": ViewFilterOperator.is_empty.value,
|
|
531
|
+
"is blank": ViewFilterOperator.is_empty.value,
|
|
532
|
+
"blank": ViewFilterOperator.is_empty.value,
|
|
533
|
+
"not_empty": ViewFilterOperator.not_empty.value,
|
|
534
|
+
"not blank": ViewFilterOperator.not_empty.value,
|
|
535
|
+
}
|
|
536
|
+
if normalized in operator_aliases:
|
|
537
|
+
payload["operator"] = operator_aliases[normalized]
|
|
538
|
+
elif "operator" not in payload:
|
|
539
|
+
payload["operator"] = normalized
|
|
540
|
+
payload.pop("op", None)
|
|
541
|
+
return payload
|
|
542
|
+
|
|
543
|
+
@model_validator(mode="after")
|
|
544
|
+
def validate_shape(self) -> "ChartFilterRulePatch":
|
|
545
|
+
if self.operator in {ViewFilterOperator.is_empty, ViewFilterOperator.not_empty}:
|
|
546
|
+
self.values = []
|
|
547
|
+
return self
|
|
548
|
+
if not self.values:
|
|
549
|
+
raise ValueError("chart filter rule requires values")
|
|
550
|
+
return self
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class ChartUpsertPatch(StrictModel):
|
|
554
|
+
chart_id: str | None = None
|
|
555
|
+
name: str
|
|
556
|
+
chart_type: PublicChartType
|
|
557
|
+
dimension_field_ids: list[str] = Field(default_factory=list)
|
|
558
|
+
indicator_field_ids: list[str] = Field(default_factory=list)
|
|
559
|
+
filters: list[ChartFilterRulePatch] = Field(default_factory=list)
|
|
560
|
+
question_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
561
|
+
user_config: list[dict[str, Any]] = Field(default_factory=list)
|
|
562
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
563
|
+
|
|
564
|
+
@model_validator(mode="before")
|
|
565
|
+
@classmethod
|
|
566
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
567
|
+
if not isinstance(value, dict):
|
|
568
|
+
return value
|
|
569
|
+
payload = dict(value)
|
|
570
|
+
if "id" in payload and "chart_id" not in payload:
|
|
571
|
+
payload["chart_id"] = payload.pop("id")
|
|
572
|
+
if "type" in payload and "chart_type" not in payload:
|
|
573
|
+
payload["chart_type"] = payload.pop("type")
|
|
574
|
+
if "dimension_fields" in payload and "dimension_field_ids" not in payload:
|
|
575
|
+
payload["dimension_field_ids"] = payload.pop("dimension_fields")
|
|
576
|
+
if "indicator_fields" in payload and "indicator_field_ids" not in payload:
|
|
577
|
+
payload["indicator_field_ids"] = payload.pop("indicator_fields")
|
|
578
|
+
if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
|
|
579
|
+
payload["indicator_field_ids"] = payload.pop("metric_field_ids")
|
|
580
|
+
raw_type = payload.get("chart_type")
|
|
581
|
+
if isinstance(raw_type, str):
|
|
582
|
+
normalized = raw_type.strip().lower()
|
|
583
|
+
aliases = {
|
|
584
|
+
"targetchart": PublicChartType.target.value,
|
|
585
|
+
"piechart": PublicChartType.pie.value,
|
|
586
|
+
"barchart": PublicChartType.bar.value,
|
|
587
|
+
"linechart": PublicChartType.line.value,
|
|
588
|
+
"tablechart": PublicChartType.table.value,
|
|
589
|
+
}
|
|
590
|
+
if normalized in aliases:
|
|
591
|
+
payload["chart_type"] = aliases[normalized]
|
|
592
|
+
if isinstance(payload.get("chart_id"), int):
|
|
593
|
+
payload["chart_id"] = str(payload["chart_id"])
|
|
594
|
+
if isinstance(payload.get("dimension_field_ids"), list):
|
|
595
|
+
payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
|
|
596
|
+
if isinstance(payload.get("indicator_field_ids"), list):
|
|
597
|
+
payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
|
|
598
|
+
return payload
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class ChartApplyRequest(StrictModel):
|
|
602
|
+
app_key: str
|
|
603
|
+
upsert_charts: list[ChartUpsertPatch] = Field(default_factory=list)
|
|
604
|
+
remove_chart_ids: list[str] = Field(default_factory=list)
|
|
605
|
+
reorder_chart_ids: list[str] = Field(default_factory=list)
|
|
606
|
+
|
|
607
|
+
@model_validator(mode="before")
|
|
608
|
+
@classmethod
|
|
609
|
+
def normalize_ids(cls, value: Any) -> Any:
|
|
610
|
+
if not isinstance(value, dict):
|
|
611
|
+
return value
|
|
612
|
+
payload = dict(value)
|
|
613
|
+
for key in ("remove_chart_ids", "reorder_chart_ids"):
|
|
614
|
+
raw = payload.get(key)
|
|
615
|
+
if isinstance(raw, list):
|
|
616
|
+
payload[key] = [str(item) for item in raw if item is not None and str(item).strip()]
|
|
617
|
+
return payload
|
|
618
|
+
|
|
619
|
+
@model_validator(mode="after")
|
|
620
|
+
def validate_shape(self) -> "ChartApplyRequest":
|
|
621
|
+
if not self.upsert_charts and not self.remove_chart_ids and not self.reorder_chart_ids:
|
|
622
|
+
raise ValueError("chart apply requires at least one upsert, remove, or reorder operation")
|
|
623
|
+
return self
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
class PortalComponentPositionPatch(StrictModel):
|
|
627
|
+
pc_x: int = Field(default=0, validation_alias=AliasChoices("pc_x", "pcX", "x"))
|
|
628
|
+
pc_y: int = Field(default=0, validation_alias=AliasChoices("pc_y", "pcY", "y"))
|
|
629
|
+
pc_w: int = Field(default=12, validation_alias=AliasChoices("pc_w", "pcW", "w"))
|
|
630
|
+
pc_h: int = Field(default=8, validation_alias=AliasChoices("pc_h", "pcH", "h"))
|
|
631
|
+
mobile_x: int = Field(default=0, validation_alias=AliasChoices("mobile_x", "mobileX"))
|
|
632
|
+
mobile_y: int = Field(default=0, validation_alias=AliasChoices("mobile_y", "mobileY"))
|
|
633
|
+
mobile_w: int = Field(default=12, validation_alias=AliasChoices("mobile_w", "mobileW"))
|
|
634
|
+
mobile_h: int = Field(default=8, validation_alias=AliasChoices("mobile_h", "mobileH"))
|
|
635
|
+
|
|
636
|
+
@model_validator(mode="before")
|
|
637
|
+
@classmethod
|
|
638
|
+
def normalize_nested_layout(cls, value: Any) -> Any:
|
|
639
|
+
if not isinstance(value, dict):
|
|
640
|
+
return value
|
|
641
|
+
payload = dict(value)
|
|
642
|
+
pc = payload.pop("pc", None)
|
|
643
|
+
mobile = payload.pop("mobile", None)
|
|
644
|
+
if isinstance(pc, dict):
|
|
645
|
+
if "pc_x" not in payload and "x" in pc:
|
|
646
|
+
payload["pc_x"] = pc.get("x")
|
|
647
|
+
if "pc_y" not in payload and "y" in pc:
|
|
648
|
+
payload["pc_y"] = pc.get("y")
|
|
649
|
+
if "pc_w" not in payload and "cols" in pc:
|
|
650
|
+
payload["pc_w"] = pc.get("cols")
|
|
651
|
+
if "pc_h" not in payload and "rows" in pc:
|
|
652
|
+
payload["pc_h"] = pc.get("rows")
|
|
653
|
+
if isinstance(mobile, dict):
|
|
654
|
+
if "mobile_x" not in payload and "x" in mobile:
|
|
655
|
+
payload["mobile_x"] = mobile.get("x")
|
|
656
|
+
if "mobile_y" not in payload and "y" in mobile:
|
|
657
|
+
payload["mobile_y"] = mobile.get("y")
|
|
658
|
+
if "mobile_w" not in payload and "cols" in mobile:
|
|
659
|
+
payload["mobile_w"] = mobile.get("cols")
|
|
660
|
+
if "mobile_h" not in payload and "rows" in mobile:
|
|
661
|
+
payload["mobile_h"] = mobile.get("rows")
|
|
662
|
+
return payload
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class PortalChartRefPatch(StrictModel):
|
|
666
|
+
app_key: str
|
|
667
|
+
chart_id: str | None = None
|
|
668
|
+
chart_name: str | None = None
|
|
669
|
+
|
|
670
|
+
@model_validator(mode="after")
|
|
671
|
+
def validate_target(self) -> "PortalChartRefPatch":
|
|
672
|
+
if not (self.chart_id or self.chart_name):
|
|
673
|
+
raise ValueError("chart_ref requires chart_id or chart_name")
|
|
674
|
+
return self
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
class PortalViewRefPatch(StrictModel):
|
|
678
|
+
app_key: str
|
|
679
|
+
view_key: str | None = None
|
|
680
|
+
view_name: str | None = None
|
|
681
|
+
|
|
682
|
+
@model_validator(mode="after")
|
|
683
|
+
def validate_target(self) -> "PortalViewRefPatch":
|
|
684
|
+
if not (self.view_key or self.view_name):
|
|
685
|
+
raise ValueError("view_ref requires view_key or view_name")
|
|
686
|
+
return self
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
class PortalSectionPatch(StrictModel):
|
|
690
|
+
title: str
|
|
691
|
+
source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
|
|
692
|
+
position: PortalComponentPositionPatch | None = None
|
|
693
|
+
dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
|
|
694
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
695
|
+
chart_ref: PortalChartRefPatch | None = None
|
|
696
|
+
view_ref: PortalViewRefPatch | None = None
|
|
697
|
+
text: str | None = None
|
|
698
|
+
url: str | None = None
|
|
699
|
+
|
|
700
|
+
@model_validator(mode="before")
|
|
701
|
+
@classmethod
|
|
702
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
703
|
+
if not isinstance(value, dict):
|
|
704
|
+
return value
|
|
705
|
+
payload = dict(value)
|
|
706
|
+
raw_type = payload.get("source_type", payload.get("sourceType"))
|
|
707
|
+
if isinstance(raw_type, str):
|
|
708
|
+
payload["source_type"] = raw_type.strip().lower()
|
|
709
|
+
if "chartRef" in payload and "chart_ref" not in payload:
|
|
710
|
+
payload["chart_ref"] = payload.pop("chartRef")
|
|
711
|
+
if "viewRef" in payload and "view_ref" not in payload:
|
|
712
|
+
payload["view_ref"] = payload.pop("viewRef")
|
|
713
|
+
if "dashStyleConfigBO" in payload and "dash_style_config" not in payload:
|
|
714
|
+
payload["dash_style_config"] = payload.pop("dashStyleConfigBO")
|
|
715
|
+
return payload
|
|
716
|
+
|
|
717
|
+
@model_validator(mode="after")
|
|
718
|
+
def validate_shape(self) -> "PortalSectionPatch":
|
|
719
|
+
supported = {"chart", "view", "grid", "filter", "text", "link"}
|
|
720
|
+
if self.source_type not in supported:
|
|
721
|
+
raise ValueError(f"unsupported portal source_type '{self.source_type}'")
|
|
722
|
+
if self.source_type == "chart" and self.chart_ref is None:
|
|
723
|
+
raise ValueError("chart section requires chart_ref")
|
|
724
|
+
if self.source_type == "view" and self.view_ref is None:
|
|
725
|
+
raise ValueError("view section requires view_ref")
|
|
726
|
+
if self.source_type == "text" and self.text is None:
|
|
727
|
+
raise ValueError("text section requires text")
|
|
728
|
+
if self.source_type == "link" and self.url is None:
|
|
729
|
+
raise ValueError("link section requires url")
|
|
730
|
+
return self
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
class PortalApplyRequest(StrictModel):
|
|
734
|
+
dash_key: str | None = None
|
|
735
|
+
dash_name: str | None = None
|
|
736
|
+
package_tag_id: int | None = None
|
|
737
|
+
publish: bool = True
|
|
738
|
+
sections: list[PortalSectionPatch] = Field(default_factory=list)
|
|
739
|
+
auth: dict[str, Any] | None = None
|
|
740
|
+
icon: str | None = None
|
|
741
|
+
color: str | None = None
|
|
742
|
+
hide_copyright: bool | None = Field(default=None, validation_alias=AliasChoices("hide_copyright", "hideCopyright"))
|
|
743
|
+
dash_global_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_global_config", "dashGlobalConfig"))
|
|
744
|
+
config: dict[str, Any] = Field(default_factory=dict)
|
|
745
|
+
|
|
746
|
+
@model_validator(mode="after")
|
|
747
|
+
def validate_shape(self) -> "PortalApplyRequest":
|
|
748
|
+
if not self.dash_key and not self.package_tag_id:
|
|
749
|
+
raise ValueError("package_tag_id is required when dash_key is empty")
|
|
750
|
+
if not self.dash_key and not self.dash_name:
|
|
751
|
+
raise ValueError("dash_name is required when creating a portal")
|
|
752
|
+
if not self.sections:
|
|
753
|
+
raise ValueError("portal apply requires a non-empty sections list")
|
|
754
|
+
return self
|
|
755
|
+
|
|
756
|
+
|
|
494
757
|
FieldPatch.model_rebuild()
|
|
495
758
|
|
|
496
759
|
|