@qingflow-tech/qingflow-app-user-mcp 1.0.39 → 1.0.41

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.
Files changed (28) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/skills/qingflow-app-builder/SKILL.md +18 -7
  5. package/skills/qingflow-app-builder/references/complete-system-development-guide.md +59 -0
  6. package/skills/qingflow-app-builder/references/create-app.md +13 -7
  7. package/skills/qingflow-app-builder/references/gotchas.md +6 -0
  8. package/skills/qingflow-app-builder/references/single-app-development-guide.md +47 -0
  9. package/skills/qingflow-app-builder/references/solution-playbooks.md +10 -0
  10. package/skills/qingflow-app-builder/references/tool-selection.md +2 -2
  11. package/skills/qingflow-app-builder-code-integrations/SKILL.md +2 -0
  12. package/skills/qingflow-app-user/SKILL.md +2 -0
  13. package/skills/qingflow-mcp-setup/SKILL.md +2 -0
  14. package/skills/qingflow-record-analysis/SKILL.md +3 -1
  15. package/skills/qingflow-record-delete/SKILL.md +2 -0
  16. package/skills/qingflow-record-import/SKILL.md +29 -0
  17. package/skills/qingflow-record-insert/SKILL.md +24 -1
  18. package/skills/qingflow-record-update/SKILL.md +3 -0
  19. package/skills/qingflow-task-ops/SKILL.md +2 -0
  20. package/src/qingflow_mcp/builder_facade/models.py +183 -0
  21. package/src/qingflow_mcp/builder_facade/service.py +722 -74
  22. package/src/qingflow_mcp/cli/commands/builder.py +62 -2
  23. package/src/qingflow_mcp/cli/commands/common.py +12 -3
  24. package/src/qingflow_mcp/cli/formatters.py +1 -0
  25. package/src/qingflow_mcp/cli/main.py +2 -0
  26. package/src/qingflow_mcp/response_trim.py +1 -0
  27. package/src/qingflow_mcp/tools/ai_builder_tools.py +515 -22
  28. package/src/qingflow_mcp/tools/record_tools.py +28 -2
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from enum import Enum
4
5
  from typing import Any
5
6
 
@@ -70,17 +71,36 @@ class PublicExternalVisibilityMode(str, Enum):
70
71
 
71
72
 
72
73
  FIELD_TYPE_ALIASES: dict[str, PublicFieldType] = {
74
+ "multiline": PublicFieldType.long_text,
75
+ "multiline_text": PublicFieldType.long_text,
76
+ "multi_line": PublicFieldType.long_text,
77
+ "multi_line_text": PublicFieldType.long_text,
73
78
  "textarea": PublicFieldType.long_text,
79
+ "longtext": PublicFieldType.long_text,
80
+ "long-text": PublicFieldType.long_text,
74
81
  "amount": PublicFieldType.amount,
75
82
  "currency": PublicFieldType.amount,
76
83
  "mobile": PublicFieldType.phone,
77
84
  "user": PublicFieldType.member,
78
85
  "users": PublicFieldType.member,
79
86
  "select": PublicFieldType.single_select,
87
+ "single_choice": PublicFieldType.single_select,
88
+ "single-choice": PublicFieldType.single_select,
89
+ "single choice": PublicFieldType.single_select,
90
+ "choice": PublicFieldType.single_select,
91
+ "dropdown": PublicFieldType.single_select,
80
92
  "radio": PublicFieldType.single_select,
81
93
  "checkbox": PublicFieldType.multi_select,
82
94
  "multi_select": PublicFieldType.multi_select,
83
95
  "multi-select": PublicFieldType.multi_select,
96
+ "multi select": PublicFieldType.multi_select,
97
+ "multiselect": PublicFieldType.multi_select,
98
+ "multi_choice": PublicFieldType.multi_select,
99
+ "multi-choice": PublicFieldType.multi_select,
100
+ "multi choice": PublicFieldType.multi_select,
101
+ "multiple_choice": PublicFieldType.multi_select,
102
+ "multiple-choice": PublicFieldType.multi_select,
103
+ "multiple choice": PublicFieldType.multi_select,
84
104
  "departments": PublicFieldType.department,
85
105
  "qlinker": PublicFieldType.q_linker,
86
106
  "q_linker": PublicFieldType.q_linker,
@@ -1796,6 +1816,65 @@ class ChartFilterRulePatch(StrictModel):
1796
1816
  return self
1797
1817
 
1798
1818
 
1819
+ class ChartMetricPatch(StrictModel):
1820
+ op: str = "count"
1821
+ field_name: str | None = Field(default=None, validation_alias=AliasChoices("field", "field_name", "fieldName", "name"))
1822
+ alias: str | None = None
1823
+
1824
+ @model_validator(mode="before")
1825
+ @classmethod
1826
+ def normalize_metric(cls, value: Any) -> Any:
1827
+ if isinstance(value, str):
1828
+ raw = value.strip()
1829
+ match = re.fullmatch(r"([A-Za-z_][A-Za-z0-9_]*)\s*\(\s*(.*?)\s*\)", raw)
1830
+ if match:
1831
+ op = match.group(1).strip().lower()
1832
+ field = match.group(2).strip()
1833
+ payload: dict[str, Any] = {"op": op}
1834
+ if field and field != "*":
1835
+ payload["field_name"] = field
1836
+ return payload
1837
+ if raw:
1838
+ return {"op": "sum", "field_name": raw}
1839
+ return {"op": "count"}
1840
+ if not isinstance(value, dict):
1841
+ return value
1842
+ payload = dict(value)
1843
+ if "operation" in payload and "op" not in payload:
1844
+ payload["op"] = payload.pop("operation")
1845
+ if "agg" in payload and "op" not in payload:
1846
+ payload["op"] = payload.pop("agg")
1847
+ if "aggregate" in payload and "op" not in payload:
1848
+ payload["op"] = payload.pop("aggregate")
1849
+ if "aggregation" in payload and "op" not in payload:
1850
+ payload["op"] = payload.pop("aggregation")
1851
+ return payload
1852
+
1853
+ @model_validator(mode="after")
1854
+ def validate_metric(self) -> "ChartMetricPatch":
1855
+ normalized_op = str(self.op or "count").strip().lower()
1856
+ op_aliases = {
1857
+ "average": "avg",
1858
+ "mean": "avg",
1859
+ "total": "sum",
1860
+ "cnt": "count",
1861
+ "count_all": "count",
1862
+ }
1863
+ self.op = op_aliases.get(normalized_op, normalized_op)
1864
+ if self.field_name is not None:
1865
+ field_name = str(self.field_name).strip()
1866
+ self.field_name = field_name or None
1867
+ if self.alias is not None:
1868
+ alias = str(self.alias).strip()
1869
+ self.alias = alias or None
1870
+ supported = {"count", "sum", "avg", "max", "min"}
1871
+ if self.op not in supported:
1872
+ raise ValueError(f"chart metric op must be one of {sorted(supported)}")
1873
+ if self.op != "count" and not self.field_name:
1874
+ raise ValueError(f"chart metric op '{self.op}' requires field")
1875
+ return self
1876
+
1877
+
1799
1878
  class ChartUpsertPatch(StrictModel):
1800
1879
  chart_id: str | None = None
1801
1880
  name: str
@@ -1803,6 +1882,17 @@ class ChartUpsertPatch(StrictModel):
1803
1882
  dimension_field_ids: list[str] = Field(default_factory=list)
1804
1883
  indicator_field_ids: list[str] = Field(default_factory=list)
1805
1884
  filters: list[ChartFilterRulePatch] = Field(default_factory=list)
1885
+ group_by: list[str] = Field(default_factory=list)
1886
+ rows: list[str] = Field(default_factory=list)
1887
+ columns: list[str] = Field(default_factory=list)
1888
+ metric: ChartMetricPatch | None = None
1889
+ metrics: list[ChartMetricPatch] = Field(default_factory=list)
1890
+ x_metric: ChartMetricPatch | None = None
1891
+ y_metric: ChartMetricPatch | None = None
1892
+ left_metric: ChartMetricPatch | None = None
1893
+ right_metric: ChartMetricPatch | None = None
1894
+ value_metric: ChartMetricPatch | None = None
1895
+ target_metric: ChartMetricPatch | None = None
1806
1896
  question_config: list[dict[str, Any]] = Field(default_factory=list)
1807
1897
  user_config: list[dict[str, Any]] = Field(default_factory=list)
1808
1898
  config: dict[str, Any] = Field(default_factory=dict)
@@ -1820,10 +1910,74 @@ class ChartUpsertPatch(StrictModel):
1820
1910
  payload["chart_type"] = payload.pop("type")
1821
1911
  if "dimension_fields" in payload and "dimension_field_ids" not in payload:
1822
1912
  payload["dimension_field_ids"] = payload.pop("dimension_fields")
1913
+ if "dimensions" in payload and "dimension_field_ids" not in payload and "group_by" not in payload:
1914
+ payload["group_by"] = payload.pop("dimensions")
1915
+ if "groupBy" in payload and "group_by" not in payload:
1916
+ payload["group_by"] = payload.pop("groupBy")
1917
+ if "where" in payload and "filters" not in payload:
1918
+ payload["filters"] = payload.pop("where")
1919
+ if "filter_rules" in payload and "filters" not in payload:
1920
+ payload["filters"] = payload.pop("filter_rules")
1921
+ if "filterRules" in payload and "filters" not in payload:
1922
+ payload["filters"] = payload.pop("filterRules")
1823
1923
  if "indicator_fields" in payload and "indicator_field_ids" not in payload:
1824
1924
  payload["indicator_field_ids"] = payload.pop("indicator_fields")
1825
1925
  if "metric_field_ids" in payload and "indicator_field_ids" not in payload:
1826
1926
  payload["indicator_field_ids"] = payload.pop("metric_field_ids")
1927
+ metric_slots: list[Any] = []
1928
+ generic_metric_keys = ("metric", "metrics")
1929
+ axis_metric_keys = (
1930
+ "x_metric",
1931
+ "xMetric",
1932
+ "y_metric",
1933
+ "yMetric",
1934
+ "left_metric",
1935
+ "leftMetric",
1936
+ "right_metric",
1937
+ "rightMetric",
1938
+ "value_metric",
1939
+ "valueMetric",
1940
+ "target_metric",
1941
+ "targetMetric",
1942
+ )
1943
+
1944
+ def has_metric_value(key: str) -> bool:
1945
+ entry = payload.get(key)
1946
+ return entry is not None and entry != "" and entry != []
1947
+
1948
+ if any(has_metric_value(key) for key in generic_metric_keys) and any(has_metric_value(key) for key in axis_metric_keys):
1949
+ raise ValueError(
1950
+ "chart metric input is ambiguous: use either metric/metrics or axis-specific "
1951
+ "x_metric/y_metric, left_metric/right_metric, value_metric/target_metric, not both"
1952
+ )
1953
+ if "metric" in payload and "metrics" not in payload:
1954
+ payload["metrics"] = [payload["metric"]]
1955
+ for key in ("x_metric", "xMetric", "left_metric", "leftMetric", "value_metric", "valueMetric"):
1956
+ if key in payload:
1957
+ slot_value = payload.get(key)
1958
+ if slot_value is not None:
1959
+ metric_slots.append(slot_value)
1960
+ canonical = {
1961
+ "xMetric": "x_metric",
1962
+ "leftMetric": "left_metric",
1963
+ "valueMetric": "value_metric",
1964
+ }.get(key)
1965
+ if canonical and canonical not in payload:
1966
+ payload[canonical] = payload.pop(key)
1967
+ for key in ("y_metric", "yMetric", "right_metric", "rightMetric", "target_metric", "targetMetric"):
1968
+ if key in payload:
1969
+ slot_value = payload.get(key)
1970
+ if slot_value is not None:
1971
+ metric_slots.append(slot_value)
1972
+ canonical = {
1973
+ "yMetric": "y_metric",
1974
+ "rightMetric": "right_metric",
1975
+ "targetMetric": "target_metric",
1976
+ }.get(key)
1977
+ if canonical and canonical not in payload:
1978
+ payload[canonical] = payload.pop(key)
1979
+ if metric_slots and "metrics" not in payload:
1980
+ payload["metrics"] = metric_slots
1827
1981
  raw_type = payload.get("chart_type")
1828
1982
  if isinstance(raw_type, str):
1829
1983
  normalized = raw_type.strip().lower()
@@ -1850,8 +2004,31 @@ class ChartUpsertPatch(StrictModel):
1850
2004
  payload["dimension_field_ids"] = [str(item) for item in payload["dimension_field_ids"] if item is not None and str(item).strip()]
1851
2005
  if isinstance(payload.get("indicator_field_ids"), list):
1852
2006
  payload["indicator_field_ids"] = [str(item) for item in payload["indicator_field_ids"] if item is not None and str(item).strip()]
2007
+ for key in ("group_by", "rows", "columns"):
2008
+ if isinstance(payload.get(key), str):
2009
+ payload[key] = [payload[key]]
2010
+ if isinstance(payload.get(key), list):
2011
+ payload[key] = [str(item) for item in payload[key] if item is not None and str(item).strip()]
1853
2012
  return payload
1854
2013
 
2014
+ @model_validator(mode="after")
2015
+ def apply_semantic_chart_fields(self) -> "ChartUpsertPatch":
2016
+ if self.group_by and not self.dimension_field_ids:
2017
+ self.dimension_field_ids = list(self.group_by)
2018
+ if self.rows and not self.dimension_field_ids:
2019
+ self.dimension_field_ids = list(self.rows)
2020
+ semantic_metrics: list[ChartMetricPatch] = []
2021
+ if self.metrics:
2022
+ semantic_metrics.extend(self.metrics)
2023
+ for metric in (self.x_metric, self.y_metric, self.left_metric, self.right_metric, self.value_metric, self.target_metric):
2024
+ if metric is not None and metric not in semantic_metrics:
2025
+ semantic_metrics.append(metric)
2026
+ if semantic_metrics and not self.metrics:
2027
+ self.metrics = semantic_metrics
2028
+ if self.metrics and not self.metric:
2029
+ self.metric = self.metrics[0]
2030
+ return self
2031
+
1855
2032
 
1856
2033
  class ChartPartialPatch(StrictModel):
1857
2034
  chart_id: str | None = None
@@ -1993,6 +2170,7 @@ class PortalViewRefPatch(StrictModel):
1993
2170
  class PortalSectionPatch(StrictModel):
1994
2171
  title: str
1995
2172
  source_type: str = Field(validation_alias=AliasChoices("source_type", "sourceType"))
2173
+ role: str | None = Field(default=None, validation_alias=AliasChoices("role", "zone", "section_role", "sectionRole"))
1996
2174
  position: PortalComponentPositionPatch | None = None
1997
2175
  dash_style_config: dict[str, Any] | None = Field(default=None, validation_alias=AliasChoices("dash_style_config", "dashStyleConfigBO"))
1998
2176
  config: dict[str, Any] = Field(default_factory=dict)
@@ -2010,6 +2188,9 @@ class PortalSectionPatch(StrictModel):
2010
2188
  raw_type = payload.get("source_type", payload.get("sourceType"))
2011
2189
  if isinstance(raw_type, str):
2012
2190
  payload["source_type"] = raw_type.strip().lower()
2191
+ raw_role = payload.get("role", payload.get("zone", payload.get("section_role", payload.get("sectionRole"))))
2192
+ if isinstance(raw_role, str):
2193
+ payload["role"] = raw_role.strip().lower()
2013
2194
  if "chartRef" in payload and "chart_ref" not in payload:
2014
2195
  payload["chart_ref"] = payload.pop("chartRef")
2015
2196
  if "viewRef" in payload and "view_ref" not in payload:
@@ -2216,6 +2397,8 @@ class ChartGetResponse(StrictModel):
2216
2397
  base: dict[str, Any] = Field(default_factory=dict)
2217
2398
  visibility: dict[str, Any] = Field(default_factory=dict)
2218
2399
  filters: list[list[dict[str, Any]]] = Field(default_factory=list)
2400
+ group_by: list[str] = Field(default_factory=list)
2401
+ metrics: list[dict[str, Any]] = Field(default_factory=list)
2219
2402
  config: dict[str, Any] = Field(default_factory=dict)
2220
2403
 
2221
2404