@josephyan/qingflow-app-builder-mcp 0.2.0-beta.1016 → 0.2.0-beta.1018
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/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/button_style_catalog.py +282 -0
- package/src/qingflow_mcp/builder_facade/models.py +44 -5
- package/src/qingflow_mcp/builder_facade/service.py +21 -8
- package/src/qingflow_mcp/cli/commands/builder.py +7 -0
- package/src/qingflow_mcp/public_surface.py +1 -0
- package/src/qingflow_mcp/response_trim.py +1 -0
- package/src/qingflow_mcp/server_app_builder.py +4 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +59 -16
- package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +73 -7
- package/src/qingflow_mcp/tools/record_tools.py +122 -7
- package/src/qingflow_mcp/tools/resource_read_tools.py +20 -10
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1018
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.1018 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
BUTTON_BACKGROUND_COLORS: tuple[str, ...] = (
|
|
7
|
+
"transparent",
|
|
8
|
+
"#FB9337",
|
|
9
|
+
"#FA6F32",
|
|
10
|
+
"#FAB300",
|
|
11
|
+
"#67C200",
|
|
12
|
+
"#00BD77",
|
|
13
|
+
"#00C5FB",
|
|
14
|
+
"#268BFB",
|
|
15
|
+
"#001A72",
|
|
16
|
+
"#9E64FB",
|
|
17
|
+
"#D164FB",
|
|
18
|
+
"#FB4B51",
|
|
19
|
+
"#FFFFFF",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
BUTTON_TEXT_COLORS: tuple[str, ...] = (
|
|
23
|
+
"#FB9337",
|
|
24
|
+
"#FA6F32",
|
|
25
|
+
"#FAB300",
|
|
26
|
+
"#67C200",
|
|
27
|
+
"#00BD77",
|
|
28
|
+
"#00C5FB",
|
|
29
|
+
"#268BFB",
|
|
30
|
+
"#001A72",
|
|
31
|
+
"#9E64FB",
|
|
32
|
+
"#D164FB",
|
|
33
|
+
"#FB4B51",
|
|
34
|
+
"#494F57",
|
|
35
|
+
"#FFFFFF",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
BUTTON_ICONS: tuple[str, ...] = (
|
|
39
|
+
"ex-print",
|
|
40
|
+
"ex-attachment",
|
|
41
|
+
"ex-handoff",
|
|
42
|
+
"ex-delete",
|
|
43
|
+
"ex-copy",
|
|
44
|
+
"ex-share",
|
|
45
|
+
"ex-edit2",
|
|
46
|
+
"ex-admin-outlined",
|
|
47
|
+
"ex-stamp",
|
|
48
|
+
"ex-kanban",
|
|
49
|
+
"ex-update",
|
|
50
|
+
"ex-tickincircle-outlined",
|
|
51
|
+
"ex-crossincircle-outlined",
|
|
52
|
+
"ex-plus-circle",
|
|
53
|
+
"ex-logout",
|
|
54
|
+
"ex-check",
|
|
55
|
+
"ex-rename",
|
|
56
|
+
"ex-locked",
|
|
57
|
+
"ex-shift",
|
|
58
|
+
"ex-new-tabpage",
|
|
59
|
+
"ex-cross",
|
|
60
|
+
"ex-switch",
|
|
61
|
+
"ex-layer",
|
|
62
|
+
"ex-import",
|
|
63
|
+
"ex-pin-outlined",
|
|
64
|
+
"ex-disabled",
|
|
65
|
+
"ex-duplicate",
|
|
66
|
+
"ex-heart-outlined",
|
|
67
|
+
"ex-plus",
|
|
68
|
+
"ex-save",
|
|
69
|
+
"ex-upload",
|
|
70
|
+
"ex-upgrade1",
|
|
71
|
+
"ex-downgrade",
|
|
72
|
+
"ex-unfold",
|
|
73
|
+
"ex-fold",
|
|
74
|
+
"ex-sort",
|
|
75
|
+
"ex-menu-control",
|
|
76
|
+
"ex-download",
|
|
77
|
+
"ex-insert-below",
|
|
78
|
+
"ex-left-outlined-double",
|
|
79
|
+
"ex-right-outlined-double",
|
|
80
|
+
"ex-recovery",
|
|
81
|
+
"ex-layout",
|
|
82
|
+
"ex-search",
|
|
83
|
+
"ex-preview",
|
|
84
|
+
"ex-invisible",
|
|
85
|
+
"ex-carboncopy",
|
|
86
|
+
"ex-basicInfo",
|
|
87
|
+
"ex-fillIn",
|
|
88
|
+
"ex-refresh",
|
|
89
|
+
"ex-display",
|
|
90
|
+
"ex-message",
|
|
91
|
+
"ex-edit",
|
|
92
|
+
"ex-heart-filled",
|
|
93
|
+
"ex-pin-filled",
|
|
94
|
+
"ex-transfer",
|
|
95
|
+
"ex-cross-circle",
|
|
96
|
+
"ex-clock",
|
|
97
|
+
"ex-admin-filled",
|
|
98
|
+
"ex-tickincircle-circle",
|
|
99
|
+
"ex-all-application",
|
|
100
|
+
"ex-help-filled",
|
|
101
|
+
"ex-streamline",
|
|
102
|
+
"ex-table",
|
|
103
|
+
"ex-rowheight-short",
|
|
104
|
+
"ex-rowheight-medium",
|
|
105
|
+
"ex-rowheight-tall",
|
|
106
|
+
"ex-rowheight-tallest",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
BUTTON_STYLE_PRESETS: tuple[dict[str, Any], ...] = (
|
|
110
|
+
{
|
|
111
|
+
"key": "primary_blue",
|
|
112
|
+
"label": "Primary Blue",
|
|
113
|
+
"button_type": "default",
|
|
114
|
+
"background_color": "#268BFB",
|
|
115
|
+
"text_color": "#FFFFFF",
|
|
116
|
+
"recommended_icons": ["ex-plus-circle", "ex-plus"],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"key": "text_blue",
|
|
120
|
+
"label": "Text Blue",
|
|
121
|
+
"button_type": "text",
|
|
122
|
+
"background_color": "transparent",
|
|
123
|
+
"text_color": "#268BFB",
|
|
124
|
+
"recommended_icons": ["ex-share", "ex-new-tabpage"],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
"key": "warning_orange",
|
|
128
|
+
"label": "Warning Orange",
|
|
129
|
+
"button_type": "default",
|
|
130
|
+
"background_color": "#FB9337",
|
|
131
|
+
"text_color": "#FFFFFF",
|
|
132
|
+
"recommended_icons": ["ex-message", "ex-clock"],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"key": "danger_red",
|
|
136
|
+
"label": "Danger Red",
|
|
137
|
+
"button_type": "default",
|
|
138
|
+
"background_color": "#FB4B51",
|
|
139
|
+
"text_color": "#FFFFFF",
|
|
140
|
+
"recommended_icons": ["ex-delete", "ex-cross-circle"],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"key": "neutral_outline",
|
|
144
|
+
"label": "Neutral Outline",
|
|
145
|
+
"button_type": "default",
|
|
146
|
+
"background_color": "#FFFFFF",
|
|
147
|
+
"text_color": "#494F57",
|
|
148
|
+
"recommended_icons": ["ex-edit", "ex-search"],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
"key": "secondary_gray",
|
|
152
|
+
"label": "Secondary Gray",
|
|
153
|
+
"button_type": "text",
|
|
154
|
+
"background_color": "transparent",
|
|
155
|
+
"text_color": "#494F57",
|
|
156
|
+
"recommended_icons": ["ex-edit", "ex-search"],
|
|
157
|
+
},
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
_PRESET_BY_KEY: dict[str, dict[str, Any]] = {
|
|
161
|
+
str(item["key"]).strip(): dict(item) for item in BUTTON_STYLE_PRESETS
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_COLOR_FAMILY_BY_VALUE: dict[str, str] = {
|
|
165
|
+
"transparent": "neutral",
|
|
166
|
+
"#FB9337": "orange",
|
|
167
|
+
"#FA6F32": "orange",
|
|
168
|
+
"#FAB300": "yellow",
|
|
169
|
+
"#67C200": "green",
|
|
170
|
+
"#00BD77": "green",
|
|
171
|
+
"#00C5FB": "cyan",
|
|
172
|
+
"#268BFB": "blue",
|
|
173
|
+
"#001A72": "blue",
|
|
174
|
+
"#9E64FB": "purple",
|
|
175
|
+
"#D164FB": "purple",
|
|
176
|
+
"#FB4B51": "red",
|
|
177
|
+
"#FFFFFF": "white",
|
|
178
|
+
"#494F57": "gray",
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _normalize_color_value(value: str | None) -> str | None:
|
|
183
|
+
normalized = str(value or "").strip()
|
|
184
|
+
if not normalized:
|
|
185
|
+
return None
|
|
186
|
+
lowered = normalized.lower().replace(" ", "")
|
|
187
|
+
if lowered == "transparent":
|
|
188
|
+
return "transparent"
|
|
189
|
+
if lowered in {
|
|
190
|
+
"#fff",
|
|
191
|
+
"#ffffff",
|
|
192
|
+
"white",
|
|
193
|
+
"rgb(255,255,255)",
|
|
194
|
+
"rgba(255,255,255,1)",
|
|
195
|
+
}:
|
|
196
|
+
return "#FFFFFF"
|
|
197
|
+
if normalized.startswith("#"):
|
|
198
|
+
return normalized.upper()
|
|
199
|
+
return normalized
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def button_style_catalog_payload() -> dict[str, Any]:
|
|
203
|
+
return {
|
|
204
|
+
"icons": [{"value": icon, "label": icon} for icon in BUTTON_ICONS],
|
|
205
|
+
"background_colors": [
|
|
206
|
+
{
|
|
207
|
+
"value": color,
|
|
208
|
+
"label": color,
|
|
209
|
+
"family": _COLOR_FAMILY_BY_VALUE.get(color, "custom"),
|
|
210
|
+
}
|
|
211
|
+
for color in BUTTON_BACKGROUND_COLORS
|
|
212
|
+
],
|
|
213
|
+
"text_colors": [
|
|
214
|
+
{
|
|
215
|
+
"value": color,
|
|
216
|
+
"label": color,
|
|
217
|
+
"family": _COLOR_FAMILY_BY_VALUE.get(color, "custom"),
|
|
218
|
+
}
|
|
219
|
+
for color in BUTTON_TEXT_COLORS
|
|
220
|
+
],
|
|
221
|
+
"presets": [deepcopy(item) for item in BUTTON_STYLE_PRESETS],
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def resolve_button_style(
|
|
226
|
+
*,
|
|
227
|
+
style_preset: str | None,
|
|
228
|
+
button_icon: str | None,
|
|
229
|
+
background_color: str | None,
|
|
230
|
+
text_color: str | None,
|
|
231
|
+
require_complete_style: bool,
|
|
232
|
+
) -> dict[str, str | None]:
|
|
233
|
+
preset_key = str(style_preset or "").strip() or None
|
|
234
|
+
resolved_icon = str(button_icon or "").strip() or None
|
|
235
|
+
resolved_background = _normalize_color_value(background_color)
|
|
236
|
+
resolved_text = _normalize_color_value(text_color)
|
|
237
|
+
|
|
238
|
+
if preset_key is not None:
|
|
239
|
+
preset = _PRESET_BY_KEY.get(preset_key)
|
|
240
|
+
if preset is None:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
"unsupported style_preset; use button_style_catalog_get to inspect available presets"
|
|
243
|
+
)
|
|
244
|
+
if resolved_background is None:
|
|
245
|
+
resolved_background = str(preset["background_color"])
|
|
246
|
+
if resolved_text is None:
|
|
247
|
+
resolved_text = str(preset["text_color"])
|
|
248
|
+
if resolved_icon is None:
|
|
249
|
+
recommended = preset.get("recommended_icons") or []
|
|
250
|
+
if recommended:
|
|
251
|
+
resolved_icon = str(recommended[0]).strip() or None
|
|
252
|
+
|
|
253
|
+
if require_complete_style:
|
|
254
|
+
missing: list[str] = []
|
|
255
|
+
if resolved_icon is None:
|
|
256
|
+
missing.append("button_icon")
|
|
257
|
+
if resolved_background is None:
|
|
258
|
+
missing.append("background_color")
|
|
259
|
+
if resolved_text is None:
|
|
260
|
+
missing.append("text_color")
|
|
261
|
+
if missing:
|
|
262
|
+
raise ValueError(f"button style requires {', '.join(missing)}")
|
|
263
|
+
|
|
264
|
+
if resolved_icon is not None and resolved_icon not in BUTTON_ICONS:
|
|
265
|
+
raise ValueError(
|
|
266
|
+
"unsupported button_icon; use button_style_catalog_get to inspect available icons"
|
|
267
|
+
)
|
|
268
|
+
if resolved_background is not None and resolved_background not in BUTTON_BACKGROUND_COLORS:
|
|
269
|
+
raise ValueError(
|
|
270
|
+
"unsupported background_color; current frontend only supports template colors from button_style_catalog_get"
|
|
271
|
+
)
|
|
272
|
+
if resolved_text is not None and resolved_text not in BUTTON_TEXT_COLORS:
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"unsupported text_color; current frontend only supports template colors from button_style_catalog_get"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"style_preset": preset_key,
|
|
279
|
+
"button_icon": resolved_icon,
|
|
280
|
+
"background_color": resolved_background,
|
|
281
|
+
"text_color": resolved_text,
|
|
282
|
+
}
|
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from pydantic import AliasChoices, Field, model_validator
|
|
7
7
|
|
|
8
|
+
from .button_style_catalog import resolve_button_style
|
|
8
9
|
from ..solution.spec_models import StrictModel
|
|
9
10
|
|
|
10
11
|
|
|
@@ -1180,10 +1181,10 @@ def _is_white_button_color(value: str | None) -> bool:
|
|
|
1180
1181
|
|
|
1181
1182
|
class CustomButtonPatch(StrictModel):
|
|
1182
1183
|
button_text: str = Field(validation_alias=AliasChoices("button_text", "buttonText"))
|
|
1183
|
-
background_color: str = Field(validation_alias=AliasChoices("background_color", "backgroundColor"))
|
|
1184
|
-
text_color: str = Field(validation_alias=AliasChoices("text_color", "textColor"))
|
|
1185
|
-
button_icon: str = Field(validation_alias=AliasChoices("button_icon", "buttonIcon"))
|
|
1186
|
-
|
|
1184
|
+
background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
|
|
1185
|
+
text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
|
|
1186
|
+
button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
|
|
1187
|
+
style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
|
|
1187
1188
|
trigger_action: PublicButtonTriggerAction = Field(validation_alias=AliasChoices("trigger_action", "triggerAction"))
|
|
1188
1189
|
trigger_link_url: str | None = Field(default=None, validation_alias=AliasChoices("trigger_link_url", "triggerLinkUrl"))
|
|
1189
1190
|
trigger_add_data_config: CustomButtonAddDataConfigPatch | None = Field(
|
|
@@ -1204,8 +1205,29 @@ class CustomButtonPatch(StrictModel):
|
|
|
1204
1205
|
validation_alias=AliasChoices("trigger_wings_config", "triggerWingsConfig"),
|
|
1205
1206
|
)
|
|
1206
1207
|
|
|
1208
|
+
@model_validator(mode="before")
|
|
1209
|
+
@classmethod
|
|
1210
|
+
def normalize_aliases(cls, value: Any) -> Any:
|
|
1211
|
+
if not isinstance(value, dict):
|
|
1212
|
+
return value
|
|
1213
|
+
payload = dict(value)
|
|
1214
|
+
if "icon_color" in payload or "iconColor" in payload:
|
|
1215
|
+
raise ValueError("icon_color is not supported; icon color follows text_color")
|
|
1216
|
+
return payload
|
|
1217
|
+
|
|
1207
1218
|
@model_validator(mode="after")
|
|
1208
1219
|
def validate_shape(self) -> "CustomButtonPatch":
|
|
1220
|
+
resolved = resolve_button_style(
|
|
1221
|
+
style_preset=self.style_preset,
|
|
1222
|
+
button_icon=self.button_icon,
|
|
1223
|
+
background_color=self.background_color,
|
|
1224
|
+
text_color=self.text_color,
|
|
1225
|
+
require_complete_style=True,
|
|
1226
|
+
)
|
|
1227
|
+
self.style_preset = resolved["style_preset"]
|
|
1228
|
+
self.button_icon = resolved["button_icon"]
|
|
1229
|
+
self.background_color = resolved["background_color"]
|
|
1230
|
+
self.text_color = resolved["text_color"]
|
|
1209
1231
|
if self.trigger_action == PublicButtonTriggerAction.link and not str(self.trigger_link_url or "").strip():
|
|
1210
1232
|
raise ValueError("link buttons require trigger_link_url")
|
|
1211
1233
|
if self.trigger_action == PublicButtonTriggerAction.add_data and self.trigger_add_data_config is None:
|
|
@@ -1225,7 +1247,7 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1225
1247
|
button_id: int = Field(validation_alias=AliasChoices("button_id", "buttonId", "id"))
|
|
1226
1248
|
button_text: str | None = Field(default=None, validation_alias=AliasChoices("button_text", "buttonText"))
|
|
1227
1249
|
button_icon: str | None = Field(default=None, validation_alias=AliasChoices("button_icon", "buttonIcon"))
|
|
1228
|
-
|
|
1250
|
+
style_preset: str | None = Field(default=None, validation_alias=AliasChoices("style_preset", "stylePreset"))
|
|
1229
1251
|
background_color: str | None = Field(default=None, validation_alias=AliasChoices("background_color", "backgroundColor"))
|
|
1230
1252
|
text_color: str | None = Field(default=None, validation_alias=AliasChoices("text_color", "textColor"))
|
|
1231
1253
|
trigger_action: str | None = Field(default=None, validation_alias=AliasChoices("trigger_action", "triggerAction"))
|
|
@@ -1244,6 +1266,8 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1244
1266
|
if not isinstance(value, dict):
|
|
1245
1267
|
return value
|
|
1246
1268
|
payload = dict(value)
|
|
1269
|
+
if "icon_color" in payload or "iconColor" in payload:
|
|
1270
|
+
raise ValueError("icon_color is not supported; icon color follows text_color")
|
|
1247
1271
|
raw_button_type = payload.get("button_type", payload.get("buttonType"))
|
|
1248
1272
|
if isinstance(raw_button_type, str):
|
|
1249
1273
|
normalized_type = raw_button_type.strip().lower()
|
|
@@ -1265,6 +1289,21 @@ class ViewButtonBindingPatch(StrictModel):
|
|
|
1265
1289
|
|
|
1266
1290
|
@model_validator(mode="after")
|
|
1267
1291
|
def validate_shape(self) -> "ViewButtonBindingPatch":
|
|
1292
|
+
require_complete_style = self.button_type == PublicViewButtonType.system or any(
|
|
1293
|
+
str(value or "").strip()
|
|
1294
|
+
for value in (self.style_preset, self.button_icon, self.background_color, self.text_color)
|
|
1295
|
+
)
|
|
1296
|
+
resolved = resolve_button_style(
|
|
1297
|
+
style_preset=self.style_preset,
|
|
1298
|
+
button_icon=self.button_icon,
|
|
1299
|
+
background_color=self.background_color,
|
|
1300
|
+
text_color=self.text_color,
|
|
1301
|
+
require_complete_style=require_complete_style,
|
|
1302
|
+
)
|
|
1303
|
+
self.style_preset = resolved["style_preset"]
|
|
1304
|
+
self.button_icon = resolved["button_icon"]
|
|
1305
|
+
self.background_color = resolved["background_color"]
|
|
1306
|
+
self.text_color = resolved["text_color"]
|
|
1268
1307
|
if self.button_type == PublicViewButtonType.system:
|
|
1269
1308
|
missing = [
|
|
1270
1309
|
field_name
|
|
@@ -34,6 +34,7 @@ from ..tools.role_tools import RoleTools
|
|
|
34
34
|
from ..tools.solution_tools import SolutionTools
|
|
35
35
|
from ..tools.view_tools import ViewTools
|
|
36
36
|
from ..tools.workflow_tools import WorkflowTools
|
|
37
|
+
from .button_style_catalog import button_style_catalog_payload
|
|
37
38
|
from .models import (
|
|
38
39
|
AppChartsReadResponse,
|
|
39
40
|
AppFieldsReadResponse,
|
|
@@ -2353,6 +2354,26 @@ class AiBuilderFacade:
|
|
|
2353
2354
|
**match,
|
|
2354
2355
|
}
|
|
2355
2356
|
|
|
2357
|
+
def button_style_catalog_get(self, *, profile: str) -> JSONObject:
|
|
2358
|
+
return {
|
|
2359
|
+
"status": "success",
|
|
2360
|
+
"error_code": None,
|
|
2361
|
+
"recoverable": False,
|
|
2362
|
+
"message": "read button style catalog",
|
|
2363
|
+
"normalized_args": {},
|
|
2364
|
+
"missing_fields": [],
|
|
2365
|
+
"allowed_values": {},
|
|
2366
|
+
"details": {},
|
|
2367
|
+
"request_id": None,
|
|
2368
|
+
"suggested_next_call": None,
|
|
2369
|
+
"noop": False,
|
|
2370
|
+
"warnings": [],
|
|
2371
|
+
"verification": {"button_style_catalog_verified": True},
|
|
2372
|
+
"verified": True,
|
|
2373
|
+
"profile": profile,
|
|
2374
|
+
**button_style_catalog_payload(),
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2356
2377
|
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
2357
2378
|
normalized_args = {"app_key": app_key}
|
|
2358
2379
|
permission_outcomes: list[PermissionCheckOutcome] = []
|
|
@@ -8035,8 +8056,6 @@ def _serialize_custom_button_payload(payload: CustomButtonPatch) -> dict[str, An
|
|
|
8035
8056
|
"buttonIcon": data["button_icon"],
|
|
8036
8057
|
"triggerAction": data["trigger_action"],
|
|
8037
8058
|
}
|
|
8038
|
-
if str(data.get("icon_color") or "").strip():
|
|
8039
|
-
serialized["iconColor"] = data["icon_color"]
|
|
8040
8059
|
if str(data.get("trigger_link_url") or "").strip():
|
|
8041
8060
|
serialized["triggerLinkUrl"] = data["trigger_link_url"]
|
|
8042
8061
|
trigger_add_data_config = data.get("trigger_add_data_config")
|
|
@@ -8126,7 +8145,6 @@ def _normalize_custom_button_summary(item: dict[str, Any]) -> dict[str, Any]:
|
|
|
8126
8145
|
"button_id": _coerce_positive_int(item.get("button_id") or item.get("buttonId") or item.get("id")),
|
|
8127
8146
|
"button_text": str(item.get("button_text") or item.get("buttonText") or "").strip() or None,
|
|
8128
8147
|
"button_icon": str(item.get("button_icon") or item.get("buttonIcon") or "").strip() or None,
|
|
8129
|
-
"icon_color": str(item.get("icon_color") or item.get("iconColor") or "").strip() or None,
|
|
8130
8148
|
"background_color": str(item.get("background_color") or item.get("backgroundColor") or "").strip() or None,
|
|
8131
8149
|
"text_color": str(item.get("text_color") or item.get("textColor") or "").strip() or None,
|
|
8132
8150
|
"used_in_chart_count": _coerce_nonnegative_int(item.get("used_in_chart_count") or item.get("userInChartCount")),
|
|
@@ -14887,7 +14905,6 @@ def _normalize_view_button_entry(entry: dict[str, Any]) -> dict[str, Any]:
|
|
|
14887
14905
|
for public_key, source_key in (
|
|
14888
14906
|
("default_button_text", "defaultButtonText"),
|
|
14889
14907
|
("button_icon", "buttonIcon"),
|
|
14890
|
-
("icon_color", "iconColor"),
|
|
14891
14908
|
("background_color", "backgroundColor"),
|
|
14892
14909
|
("text_color", "textColor"),
|
|
14893
14910
|
("trigger_link_url", "triggerLinkUrl"),
|
|
@@ -14925,7 +14942,6 @@ def _normalize_view_buttons_for_compare(value: Any) -> list[dict[str, Any]]:
|
|
|
14925
14942
|
"button_id": normalized.get("button_id") if is_custom else None,
|
|
14926
14943
|
"button_text": normalized.get("button_text"),
|
|
14927
14944
|
"button_icon": normalized.get("button_icon"),
|
|
14928
|
-
"icon_color": normalized.get("icon_color"),
|
|
14929
14945
|
"background_color": normalized.get("background_color"),
|
|
14930
14946
|
"text_color": normalized.get("text_color"),
|
|
14931
14947
|
"trigger_action": normalized.get("trigger_action"),
|
|
@@ -15000,7 +15016,6 @@ def _normalize_expected_view_buttons_for_compare(
|
|
|
15000
15016
|
for key in (
|
|
15001
15017
|
"button_text",
|
|
15002
15018
|
"button_icon",
|
|
15003
|
-
"icon_color",
|
|
15004
15019
|
"background_color",
|
|
15005
15020
|
"text_color",
|
|
15006
15021
|
"trigger_action",
|
|
@@ -15183,8 +15198,6 @@ def _serialize_view_button_binding(
|
|
|
15183
15198
|
if binding.button_type in {PublicViewButtonType.system, PublicViewButtonType.custom}:
|
|
15184
15199
|
dto["buttonText"] = binding.button_text
|
|
15185
15200
|
dto["buttonIcon"] = binding.button_icon
|
|
15186
|
-
if str(binding.icon_color or "").strip():
|
|
15187
|
-
dto["iconColor"] = binding.icon_color
|
|
15188
15201
|
dto["backgroundColor"] = binding.background_color
|
|
15189
15202
|
dto["textColor"] = binding.text_color
|
|
15190
15203
|
dto["triggerAction"] = binding.trigger_action
|
|
@@ -120,6 +120,9 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
120
120
|
button = builder_subparsers.add_parser("button", help="自定义按钮")
|
|
121
121
|
button_subparsers = button.add_subparsers(dest="builder_button_command", required=True)
|
|
122
122
|
|
|
123
|
+
button_catalog = button_subparsers.add_parser("catalog", help="读取按钮样式目录")
|
|
124
|
+
button_catalog.set_defaults(handler=_handle_button_catalog, format_hint="builder_summary")
|
|
125
|
+
|
|
123
126
|
button_list = button_subparsers.add_parser("list", help="列出自定义按钮")
|
|
124
127
|
button_list.add_argument("--app-key", required=True)
|
|
125
128
|
button_list.set_defaults(handler=_handle_button_list, format_hint="builder_summary")
|
|
@@ -362,6 +365,10 @@ def _handle_button_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
362
365
|
return context.builder.app_custom_button_list(profile=args.profile, app_key=args.app_key)
|
|
363
366
|
|
|
364
367
|
|
|
368
|
+
def _handle_button_catalog(args: argparse.Namespace, context: CliContext) -> dict:
|
|
369
|
+
return context.builder.button_style_catalog_get(profile=args.profile)
|
|
370
|
+
|
|
371
|
+
|
|
365
372
|
def _handle_button_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
366
373
|
return context.builder.app_custom_button_get(profile=args.profile, app_key=args.app_key, button_id=args.button_id)
|
|
367
374
|
|
|
@@ -134,6 +134,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
134
134
|
PublicToolSpec(BUILDER_DOMAIN, "role_create", ("role_create",), ("builder", "role", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
135
135
|
PublicToolSpec(BUILDER_DOMAIN, "app_release_edit_lock_if_mine", ("app_release_edit_lock_if_mine",), ("builder", "app", "release-edit-lock-if-mine"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
136
136
|
PublicToolSpec(BUILDER_DOMAIN, "app_resolve", ("app_resolve",), ("builder", "app", "resolve"), has_contract=True, cli_show_effective_context=True),
|
|
137
|
+
PublicToolSpec(BUILDER_DOMAIN, "button_style_catalog_get", ("button_style_catalog_get",), ("builder", "button", "catalog"), has_contract=True, cli_show_effective_context=True),
|
|
137
138
|
PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_list", ("app_custom_button_list",), ("builder", "button", "list"), has_contract=True, cli_show_effective_context=True),
|
|
138
139
|
PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_get", ("app_custom_button_get",), ("builder", "button", "get"), has_contract=True, cli_show_effective_context=True),
|
|
139
140
|
PublicToolSpec(BUILDER_DOMAIN, "app_custom_button_create", ("app_custom_button_create",), ("builder", "button", "create"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
@@ -290,6 +290,10 @@ def build_builder_server() -> FastMCP:
|
|
|
290
290
|
)
|
|
291
291
|
return ai_builder.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_id=package_id)
|
|
292
292
|
|
|
293
|
+
@server.tool()
|
|
294
|
+
def button_style_catalog_get(profile: str = DEFAULT_PROFILE) -> dict:
|
|
295
|
+
return ai_builder.button_style_catalog_get(profile=profile)
|
|
296
|
+
|
|
293
297
|
@server.tool()
|
|
294
298
|
def app_custom_button_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
295
299
|
return ai_builder.app_custom_button_list(profile=profile, app_key=app_key)
|
|
@@ -6,6 +6,12 @@ import time
|
|
|
6
6
|
|
|
7
7
|
from pydantic import ValidationError
|
|
8
8
|
|
|
9
|
+
from ..builder_facade.button_style_catalog import (
|
|
10
|
+
BUTTON_BACKGROUND_COLORS,
|
|
11
|
+
BUTTON_ICONS,
|
|
12
|
+
BUTTON_STYLE_PRESETS,
|
|
13
|
+
BUTTON_TEXT_COLORS,
|
|
14
|
+
)
|
|
9
15
|
from ..public_surface import public_builder_contract_tool_names
|
|
10
16
|
from ..config import DEFAULT_PROFILE
|
|
11
17
|
from ..errors import QingflowApiError
|
|
@@ -209,6 +215,10 @@ class AiBuilderTools(ToolBase):
|
|
|
209
215
|
)
|
|
210
216
|
return self.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_id=package_id)
|
|
211
217
|
|
|
218
|
+
@mcp.tool()
|
|
219
|
+
def button_style_catalog_get(profile: str = DEFAULT_PROFILE) -> JSONObject:
|
|
220
|
+
return self.button_style_catalog_get(profile=profile)
|
|
221
|
+
|
|
212
222
|
@mcp.tool()
|
|
213
223
|
def app_custom_button_list(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
214
224
|
return self.app_custom_button_list(profile=profile, app_key=app_key)
|
|
@@ -846,6 +856,17 @@ class AiBuilderTools(ToolBase):
|
|
|
846
856
|
suggested_next_call={"tool_name": "app_resolve", "arguments": {"profile": profile, **normalized_args}},
|
|
847
857
|
))
|
|
848
858
|
|
|
859
|
+
@tool_cn_name("按钮样式目录")
|
|
860
|
+
def button_style_catalog_get(self, *, profile: str) -> JSONObject:
|
|
861
|
+
"""执行按钮样式相关逻辑。"""
|
|
862
|
+
normalized_args: dict[str, object] = {}
|
|
863
|
+
return _safe_tool_call(
|
|
864
|
+
lambda: self._facade.button_style_catalog_get(profile=profile),
|
|
865
|
+
error_code="BUTTON_STYLE_CATALOG_GET_FAILED",
|
|
866
|
+
normalized_args=normalized_args,
|
|
867
|
+
suggested_next_call={"tool_name": "button_style_catalog_get", "arguments": {"profile": profile}},
|
|
868
|
+
)
|
|
869
|
+
|
|
849
870
|
@tool_cn_name("应用按钮列表")
|
|
850
871
|
def app_custom_button_list(self, *, profile: str, app_key: str) -> JSONObject:
|
|
851
872
|
"""执行应用相关逻辑。"""
|
|
@@ -885,9 +906,8 @@ class AiBuilderTools(ToolBase):
|
|
|
885
906
|
"app_key": app_key,
|
|
886
907
|
"payload": {
|
|
887
908
|
"button_text": "新增记录",
|
|
888
|
-
"
|
|
889
|
-
"
|
|
890
|
-
"button_icon": "ex-add-outlined",
|
|
909
|
+
"style_preset": "primary_blue",
|
|
910
|
+
"button_icon": "ex-plus-circle",
|
|
891
911
|
"trigger_action": "addData",
|
|
892
912
|
"trigger_add_data_config": {"related_app_key": "TARGET_APP_KEY", "que_relation": []},
|
|
893
913
|
},
|
|
@@ -920,9 +940,8 @@ class AiBuilderTools(ToolBase):
|
|
|
920
940
|
"button_id": button_id,
|
|
921
941
|
"payload": {
|
|
922
942
|
"button_text": "新增记录",
|
|
923
|
-
"
|
|
924
|
-
"
|
|
925
|
-
"button_icon": "ex-add-outlined",
|
|
943
|
+
"style_preset": "neutral_outline",
|
|
944
|
+
"button_icon": "ex-edit",
|
|
926
945
|
"trigger_action": "link",
|
|
927
946
|
"trigger_link_url": "https://example.com",
|
|
928
947
|
},
|
|
@@ -2535,6 +2554,24 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2535
2554
|
"lock_owner_name": "当前用户",
|
|
2536
2555
|
},
|
|
2537
2556
|
},
|
|
2557
|
+
"button_style_catalog_get": {
|
|
2558
|
+
"allowed_keys": [],
|
|
2559
|
+
"aliases": {},
|
|
2560
|
+
"allowed_values": {
|
|
2561
|
+
"preset.key": [item["key"] for item in BUTTON_STYLE_PRESETS],
|
|
2562
|
+
"icon": list(BUTTON_ICONS),
|
|
2563
|
+
"background_color": list(BUTTON_BACKGROUND_COLORS),
|
|
2564
|
+
"text_color": list(BUTTON_TEXT_COLORS),
|
|
2565
|
+
},
|
|
2566
|
+
"execution_notes": [
|
|
2567
|
+
"use this read-only tool before button writes when an agent needs a supported icon or color choice",
|
|
2568
|
+
"current frontend only supports template icons and template colors from this catalog",
|
|
2569
|
+
"text/icon color is unified through text_color; there is no separate icon_color",
|
|
2570
|
+
],
|
|
2571
|
+
"minimal_example": {
|
|
2572
|
+
"profile": "default",
|
|
2573
|
+
},
|
|
2574
|
+
},
|
|
2538
2575
|
"app_custom_button_list": {
|
|
2539
2576
|
"allowed_keys": ["app_key"],
|
|
2540
2577
|
"aliases": {},
|
|
@@ -2558,10 +2595,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2558
2595
|
"allowed_keys": ["app_key", "payload"],
|
|
2559
2596
|
"aliases": {
|
|
2560
2597
|
"payload.buttonText": "payload.button_text",
|
|
2598
|
+
"payload.stylePreset": "payload.style_preset",
|
|
2561
2599
|
"payload.backgroundColor": "payload.background_color",
|
|
2562
2600
|
"payload.textColor": "payload.text_color",
|
|
2563
2601
|
"payload.buttonIcon": "payload.button_icon",
|
|
2564
|
-
"payload.iconColor": "payload.icon_color",
|
|
2565
2602
|
"payload.triggerAction": "payload.trigger_action",
|
|
2566
2603
|
"payload.triggerLinkUrl": "payload.trigger_link_url",
|
|
2567
2604
|
"payload.triggerAddDataConfig": "payload.trigger_add_data_config",
|
|
@@ -2571,10 +2608,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2571
2608
|
},
|
|
2572
2609
|
"allowed_values": {
|
|
2573
2610
|
"payload.trigger_action": [member.value for member in PublicButtonTriggerAction],
|
|
2611
|
+
"payload.style_preset": [item["key"] for item in BUTTON_STYLE_PRESETS],
|
|
2612
|
+
"payload.button_icon": list(BUTTON_ICONS),
|
|
2613
|
+
"payload.background_color": list(BUTTON_BACKGROUND_COLORS),
|
|
2614
|
+
"payload.text_color": list(BUTTON_TEXT_COLORS),
|
|
2574
2615
|
},
|
|
2575
2616
|
"execution_notes": [
|
|
2576
2617
|
"custom button writes now auto-publish the current app draft as a fixed closing step",
|
|
2577
2618
|
"background_color and text_color cannot both be white",
|
|
2619
|
+
"payload accepts either style_preset + optional button_icon, or explicit button_icon/background_color/text_color from button_style_catalog_get",
|
|
2578
2620
|
"for addData buttons, put field mappings in payload.trigger_add_data_config.que_relation",
|
|
2579
2621
|
],
|
|
2580
2622
|
"minimal_example": {
|
|
@@ -2582,10 +2624,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2582
2624
|
"app_key": "APP_KEY",
|
|
2583
2625
|
"payload": {
|
|
2584
2626
|
"button_text": "新增记录",
|
|
2585
|
-
"
|
|
2586
|
-
"
|
|
2587
|
-
"button_icon": "ex-add-outlined",
|
|
2588
|
-
"icon_color": "#494F57",
|
|
2627
|
+
"style_preset": "primary_blue",
|
|
2628
|
+
"button_icon": "ex-plus-circle",
|
|
2589
2629
|
"trigger_action": "link",
|
|
2590
2630
|
"trigger_link_url": "https://example.com",
|
|
2591
2631
|
},
|
|
@@ -2596,10 +2636,10 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2596
2636
|
"aliases": {
|
|
2597
2637
|
"buttonId": "button_id",
|
|
2598
2638
|
"payload.buttonText": "payload.button_text",
|
|
2639
|
+
"payload.stylePreset": "payload.style_preset",
|
|
2599
2640
|
"payload.backgroundColor": "payload.background_color",
|
|
2600
2641
|
"payload.textColor": "payload.text_color",
|
|
2601
2642
|
"payload.buttonIcon": "payload.button_icon",
|
|
2602
|
-
"payload.iconColor": "payload.icon_color",
|
|
2603
2643
|
"payload.triggerAction": "payload.trigger_action",
|
|
2604
2644
|
"payload.triggerLinkUrl": "payload.trigger_link_url",
|
|
2605
2645
|
"payload.triggerAddDataConfig": "payload.trigger_add_data_config",
|
|
@@ -2609,10 +2649,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2609
2649
|
},
|
|
2610
2650
|
"allowed_values": {
|
|
2611
2651
|
"payload.trigger_action": [member.value for member in PublicButtonTriggerAction],
|
|
2652
|
+
"payload.style_preset": [item["key"] for item in BUTTON_STYLE_PRESETS],
|
|
2653
|
+
"payload.button_icon": list(BUTTON_ICONS),
|
|
2654
|
+
"payload.background_color": list(BUTTON_BACKGROUND_COLORS),
|
|
2655
|
+
"payload.text_color": list(BUTTON_TEXT_COLORS),
|
|
2612
2656
|
},
|
|
2613
2657
|
"execution_notes": [
|
|
2614
2658
|
"custom button writes now auto-publish the current app draft as a fixed closing step",
|
|
2615
2659
|
"background_color and text_color cannot both be white",
|
|
2660
|
+
"payload accepts either style_preset + optional button_icon, or explicit button_icon/background_color/text_color from button_style_catalog_get",
|
|
2616
2661
|
"for addData buttons, put field mappings in payload.trigger_add_data_config.que_relation",
|
|
2617
2662
|
],
|
|
2618
2663
|
"minimal_example": {
|
|
@@ -2621,10 +2666,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2621
2666
|
"button_id": 1001,
|
|
2622
2667
|
"payload": {
|
|
2623
2668
|
"button_text": "查看详情",
|
|
2624
|
-
"
|
|
2625
|
-
"
|
|
2626
|
-
"button_icon": "ex-link-outlined",
|
|
2627
|
-
"icon_color": "#494F57",
|
|
2669
|
+
"style_preset": "neutral_outline",
|
|
2670
|
+
"button_icon": "ex-edit",
|
|
2628
2671
|
"trigger_action": "link",
|
|
2629
2672
|
"trigger_link_url": "https://example.com/detail",
|
|
2630
2673
|
},
|
|
@@ -169,7 +169,6 @@ class CustomButtonTools(ToolBase):
|
|
|
169
169
|
"button_id": item.get("buttonId"),
|
|
170
170
|
"button_text": item.get("buttonText"),
|
|
171
171
|
"button_icon": item.get("buttonIcon"),
|
|
172
|
-
"icon_color": item.get("iconColor"),
|
|
173
172
|
"background_color": item.get("backgroundColor"),
|
|
174
173
|
"text_color": item.get("textColor"),
|
|
175
174
|
"creator_user_info": {
|
|
@@ -189,7 +188,6 @@ class CustomButtonTools(ToolBase):
|
|
|
189
188
|
"button_id": item.get("buttonId"),
|
|
190
189
|
"button_text": item.get("buttonText"),
|
|
191
190
|
"button_icon": item.get("buttonIcon"),
|
|
192
|
-
"icon_color": item.get("iconColor"),
|
|
193
191
|
"background_color": item.get("backgroundColor"),
|
|
194
192
|
"text_color": item.get("textColor"),
|
|
195
193
|
"trigger_action": item.get("triggerAction"),
|
|
@@ -11,6 +11,7 @@ from uuid import uuid4
|
|
|
11
11
|
|
|
12
12
|
from mcp.server.fastmcp import FastMCP
|
|
13
13
|
|
|
14
|
+
from ..backend_client import BackendRequestContext
|
|
14
15
|
from ..config import DEFAULT_PROFILE, DEFAULT_RECORD_LIST_TYPE
|
|
15
16
|
from ..errors import QingflowApiError
|
|
16
17
|
from ..export_store import ExportJobStore
|
|
@@ -213,6 +214,10 @@ class ExportTools(ToolBase):
|
|
|
213
214
|
{
|
|
214
215
|
"created_at": started_at,
|
|
215
216
|
"profile": profile,
|
|
217
|
+
"base_url": context.base_url,
|
|
218
|
+
"ws_id": context.ws_id,
|
|
219
|
+
"qf_version": context.qf_version,
|
|
220
|
+
"qf_version_source": context.qf_version_source,
|
|
216
221
|
"app_key": normalized_app_key,
|
|
217
222
|
"view_id": resolved_view.view_id,
|
|
218
223
|
"backend_export_id": str(socket_result.get("backend_export_id") or ""),
|
|
@@ -280,7 +285,7 @@ class ExportTools(ToolBase):
|
|
|
280
285
|
extra={"status": "failed"},
|
|
281
286
|
)
|
|
282
287
|
|
|
283
|
-
def runner(
|
|
288
|
+
def runner(session_profile, context):
|
|
284
289
|
local_job = self._job_store.get(normalized_handle)
|
|
285
290
|
if local_job is None:
|
|
286
291
|
return self._failed_export_result(
|
|
@@ -288,7 +293,13 @@ class ExportTools(ToolBase):
|
|
|
288
293
|
message="export_handle is missing or expired",
|
|
289
294
|
extra={"export_handle": normalized_handle, "status": "failed"},
|
|
290
295
|
)
|
|
291
|
-
|
|
296
|
+
lookup_context = self._build_export_lookup_context(
|
|
297
|
+
profile=profile,
|
|
298
|
+
session_profile=session_profile,
|
|
299
|
+
current_context=context,
|
|
300
|
+
local_job=local_job,
|
|
301
|
+
)
|
|
302
|
+
snapshot = self._resolve_export_snapshot(lookup_context, local_job)
|
|
292
303
|
return self._status_payload_from_snapshot(local_job, normalized_handle, snapshot)
|
|
293
304
|
|
|
294
305
|
try:
|
|
@@ -316,7 +327,7 @@ class ExportTools(ToolBase):
|
|
|
316
327
|
extra={"status": "failed"},
|
|
317
328
|
)
|
|
318
329
|
|
|
319
|
-
def runner(
|
|
330
|
+
def runner(session_profile, context):
|
|
320
331
|
local_job = self._job_store.get(normalized_handle)
|
|
321
332
|
if local_job is None:
|
|
322
333
|
return self._failed_export_result(
|
|
@@ -324,7 +335,13 @@ class ExportTools(ToolBase):
|
|
|
324
335
|
message="export_handle is missing or expired",
|
|
325
336
|
extra={"export_handle": normalized_handle, "status": "failed"},
|
|
326
337
|
)
|
|
327
|
-
|
|
338
|
+
lookup_context = self._build_export_lookup_context(
|
|
339
|
+
profile=profile,
|
|
340
|
+
session_profile=session_profile,
|
|
341
|
+
current_context=context,
|
|
342
|
+
local_job=local_job,
|
|
343
|
+
)
|
|
344
|
+
snapshot = self._resolve_export_snapshot(lookup_context, local_job)
|
|
328
345
|
normalized_status = str(snapshot.get("status") or "unknown")
|
|
329
346
|
if normalized_status not in {"succeeded", "failed"}:
|
|
330
347
|
return self._failed_export_result(
|
|
@@ -414,7 +431,7 @@ class ExportTools(ToolBase):
|
|
|
414
431
|
"downloaded_files": downloaded_files,
|
|
415
432
|
"warnings": snapshot.get("warnings") or [],
|
|
416
433
|
"verification": snapshot.get("verification") or {},
|
|
417
|
-
"request_route": self.backend.describe_route(
|
|
434
|
+
"request_route": self.backend.describe_route(lookup_context),
|
|
418
435
|
}
|
|
419
436
|
|
|
420
437
|
try:
|
|
@@ -451,7 +468,7 @@ class ExportTools(ToolBase):
|
|
|
451
468
|
)
|
|
452
469
|
timeout_seconds = self._normalize_timeout_seconds(wait_timeout_seconds)
|
|
453
470
|
|
|
454
|
-
def runner(
|
|
471
|
+
def runner(session_profile, context):
|
|
455
472
|
start_result = self.record_export_start(
|
|
456
473
|
profile=profile,
|
|
457
474
|
app_key=normalized_app_key,
|
|
@@ -475,7 +492,13 @@ class ExportTools(ToolBase):
|
|
|
475
492
|
message="export_handle is missing or expired",
|
|
476
493
|
extra={"status": "failed", "export_handle": export_handle},
|
|
477
494
|
)
|
|
478
|
-
|
|
495
|
+
lookup_context = self._build_export_lookup_context(
|
|
496
|
+
profile=profile,
|
|
497
|
+
session_profile=session_profile,
|
|
498
|
+
current_context=context,
|
|
499
|
+
local_job=local_job,
|
|
500
|
+
)
|
|
501
|
+
snapshot = self._resolve_export_snapshot(lookup_context, local_job)
|
|
479
502
|
last_snapshot = snapshot
|
|
480
503
|
normalized_status = str(snapshot.get("status") or "unknown")
|
|
481
504
|
if normalized_status == "succeeded":
|
|
@@ -583,6 +606,49 @@ class ExportTools(ToolBase):
|
|
|
583
606
|
extra={"app_key": normalized_app_key, "view_id": normalized_view_id},
|
|
584
607
|
)
|
|
585
608
|
|
|
609
|
+
def _build_export_lookup_context(
|
|
610
|
+
self,
|
|
611
|
+
*,
|
|
612
|
+
profile: str,
|
|
613
|
+
session_profile,
|
|
614
|
+
current_context: BackendRequestContext,
|
|
615
|
+
local_job: dict[str, Any],
|
|
616
|
+
) -> BackendRequestContext:
|
|
617
|
+
stored_profile = str(local_job.get("profile") or "").strip()
|
|
618
|
+
if stored_profile and stored_profile != profile:
|
|
619
|
+
raise QingflowApiError.config_error(
|
|
620
|
+
"export_handle was created under a different profile",
|
|
621
|
+
details={
|
|
622
|
+
"error_code": "EXPORT_HANDLE_PROFILE_MISMATCH",
|
|
623
|
+
"expected_profile": stored_profile,
|
|
624
|
+
"received_profile": profile,
|
|
625
|
+
},
|
|
626
|
+
)
|
|
627
|
+
stored_uid = _coerce_positive_int(local_job.get("uid"))
|
|
628
|
+
if stored_uid is not None and stored_uid != session_profile.uid:
|
|
629
|
+
raise QingflowApiError.config_error(
|
|
630
|
+
"export_handle belongs to a different authenticated user",
|
|
631
|
+
details={
|
|
632
|
+
"error_code": "EXPORT_HANDLE_OWNER_MISMATCH",
|
|
633
|
+
"expected_uid": stored_uid,
|
|
634
|
+
"current_uid": session_profile.uid,
|
|
635
|
+
},
|
|
636
|
+
)
|
|
637
|
+
stored_base_url = str(local_job.get("base_url") or "").strip() or current_context.base_url
|
|
638
|
+
stored_ws_id = _coerce_positive_int(local_job.get("ws_id"))
|
|
639
|
+
stored_qf_version = str(local_job.get("qf_version") or "").strip() or current_context.qf_version
|
|
640
|
+
stored_qf_version_source = (
|
|
641
|
+
str(local_job.get("qf_version_source") or "").strip() or current_context.qf_version_source
|
|
642
|
+
)
|
|
643
|
+
return BackendRequestContext(
|
|
644
|
+
base_url=stored_base_url,
|
|
645
|
+
token=current_context.token,
|
|
646
|
+
ws_id=stored_ws_id if stored_ws_id is not None else current_context.ws_id,
|
|
647
|
+
qf_request_id=current_context.qf_request_id,
|
|
648
|
+
qf_version=stored_qf_version,
|
|
649
|
+
qf_version_source=stored_qf_version_source,
|
|
650
|
+
)
|
|
651
|
+
|
|
586
652
|
def _resolve_export_record_scope(
|
|
587
653
|
self,
|
|
588
654
|
*,
|
|
@@ -2920,12 +2920,14 @@ class RecordTools(ToolBase):
|
|
|
2920
2920
|
payload["category"] = error.category
|
|
2921
2921
|
if error.backend_code is not None:
|
|
2922
2922
|
payload["backend_code"] = error.backend_code
|
|
2923
|
+
if error.request_id is not None:
|
|
2924
|
+
payload["request_id"] = error.request_id
|
|
2923
2925
|
if error.http_status is not None:
|
|
2924
2926
|
payload["http_status"] = error.http_status
|
|
2925
2927
|
return payload
|
|
2926
2928
|
|
|
2927
2929
|
def _is_record_context_route_miss(self, error: QingflowApiError) -> bool:
|
|
2928
|
-
if error.backend_code in {40002, 40027, 40038, 404}:
|
|
2930
|
+
if error.backend_code in {40002, 40023, 40027, 40038, 404}:
|
|
2929
2931
|
return True
|
|
2930
2932
|
if error.http_status == 404:
|
|
2931
2933
|
return True
|
|
@@ -3059,6 +3061,107 @@ class RecordTools(ToolBase):
|
|
|
3059
3061
|
f"({get_record_list_type_label(used_list_type)})."
|
|
3060
3062
|
]
|
|
3061
3063
|
|
|
3064
|
+
def _looks_like_generic_record_update_backend_failure(self, exc: QingflowApiError) -> bool:
|
|
3065
|
+
if exc.backend_code == 500:
|
|
3066
|
+
return True
|
|
3067
|
+
if exc.http_status is not None and exc.http_status >= 500:
|
|
3068
|
+
return True
|
|
3069
|
+
normalized_message = exc.message.strip().lower()
|
|
3070
|
+
return normalized_message in {"unknown error", "internal server error"}
|
|
3071
|
+
|
|
3072
|
+
def _remap_record_update_target_context_error(
|
|
3073
|
+
self,
|
|
3074
|
+
profile: str,
|
|
3075
|
+
context, # type: ignore[no-untyped-def]
|
|
3076
|
+
*,
|
|
3077
|
+
app_key: str,
|
|
3078
|
+
apply_id: int,
|
|
3079
|
+
exc: QingflowApiError,
|
|
3080
|
+
) -> None:
|
|
3081
|
+
if not self._looks_like_generic_record_update_backend_failure(exc):
|
|
3082
|
+
return
|
|
3083
|
+
try:
|
|
3084
|
+
candidate_routes = self._candidate_update_views(profile, context, app_key)
|
|
3085
|
+
probes = self._probe_candidate_record_contexts(
|
|
3086
|
+
context,
|
|
3087
|
+
app_key=app_key,
|
|
3088
|
+
apply_id=apply_id,
|
|
3089
|
+
candidate_routes=candidate_routes,
|
|
3090
|
+
)
|
|
3091
|
+
except (QingflowApiError, RuntimeError):
|
|
3092
|
+
return
|
|
3093
|
+
if not probes or any(probe.readable for probe in probes):
|
|
3094
|
+
return
|
|
3095
|
+
|
|
3096
|
+
blocker = (
|
|
3097
|
+
"CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3098
|
+
if all(probe.transport_error for probe in probes)
|
|
3099
|
+
else "NO_MATCHING_ACCESSIBLE_VIEW_FOR_RECORD"
|
|
3100
|
+
)
|
|
3101
|
+
recommended_next_actions = (
|
|
3102
|
+
[
|
|
3103
|
+
"Retry after the record becomes readable in the current workspace/profile context.",
|
|
3104
|
+
"If the issue persists, verify that the current profile still has read access to this record.",
|
|
3105
|
+
]
|
|
3106
|
+
if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3107
|
+
else [
|
|
3108
|
+
"Use record_get or record_list to confirm the record still exists in the current workspace.",
|
|
3109
|
+
"Call record_update_schema_get to inspect whether any accessible view still matches this record.",
|
|
3110
|
+
]
|
|
3111
|
+
)
|
|
3112
|
+
first_error_payload = next(
|
|
3113
|
+
(
|
|
3114
|
+
cast(JSONObject, probe.error_payload)
|
|
3115
|
+
for probe in probes
|
|
3116
|
+
if isinstance(probe.error_payload, dict)
|
|
3117
|
+
),
|
|
3118
|
+
None,
|
|
3119
|
+
)
|
|
3120
|
+
backend_code = (
|
|
3121
|
+
cast(int, first_error_payload.get("backend_code"))
|
|
3122
|
+
if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("backend_code"), int)
|
|
3123
|
+
else exc.backend_code
|
|
3124
|
+
)
|
|
3125
|
+
request_id = (
|
|
3126
|
+
_normalize_optional_text(first_error_payload.get("request_id"))
|
|
3127
|
+
if isinstance(first_error_payload, dict)
|
|
3128
|
+
else None
|
|
3129
|
+
) or exc.request_id
|
|
3130
|
+
http_status = (
|
|
3131
|
+
cast(int, first_error_payload.get("http_status"))
|
|
3132
|
+
if isinstance(first_error_payload, dict) and isinstance(first_error_payload.get("http_status"), int)
|
|
3133
|
+
else exc.http_status
|
|
3134
|
+
)
|
|
3135
|
+
raise_tool_error(
|
|
3136
|
+
QingflowApiError(
|
|
3137
|
+
category="backend",
|
|
3138
|
+
message=(
|
|
3139
|
+
"Direct record edit was blocked because the current record context could not be loaded from any candidate route."
|
|
3140
|
+
if blocker == "CURRENT_RECORD_CONTEXT_UNAVAILABLE"
|
|
3141
|
+
else "Direct record edit was blocked because the target record is no longer accessible in any readable view for the current profile."
|
|
3142
|
+
),
|
|
3143
|
+
backend_code=backend_code,
|
|
3144
|
+
request_id=request_id,
|
|
3145
|
+
http_status=http_status,
|
|
3146
|
+
details={
|
|
3147
|
+
"error_code": blocker,
|
|
3148
|
+
"operation": "update",
|
|
3149
|
+
"app_key": app_key,
|
|
3150
|
+
"record_id": apply_id,
|
|
3151
|
+
"blockers": [blocker],
|
|
3152
|
+
"request_route": self._request_route_payload(context),
|
|
3153
|
+
"view_probe_summary": [
|
|
3154
|
+
self._record_context_probe_summary_payload(probe)
|
|
3155
|
+
for probe in probes
|
|
3156
|
+
],
|
|
3157
|
+
"recommended_next_actions": recommended_next_actions,
|
|
3158
|
+
"fix_hint": (
|
|
3159
|
+
"Confirm the target record still exists and remains visible in at least one accessible view before retrying the update."
|
|
3160
|
+
),
|
|
3161
|
+
},
|
|
3162
|
+
)
|
|
3163
|
+
)
|
|
3164
|
+
|
|
3062
3165
|
def _record_matches_accessible_view(
|
|
3063
3166
|
self,
|
|
3064
3167
|
context, # type: ignore[no-untyped-def]
|
|
@@ -6355,12 +6458,22 @@ class RecordTools(ToolBase):
|
|
|
6355
6458
|
force_refresh_form=force_refresh_form,
|
|
6356
6459
|
)
|
|
6357
6460
|
self._validate_record_write(app_key, normalized_answers, apply_id=normalized_apply_id)
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6461
|
+
try:
|
|
6462
|
+
result = self.backend.request(
|
|
6463
|
+
"POST",
|
|
6464
|
+
context,
|
|
6465
|
+
f"/app/{app_key}/apply/{normalized_apply_id}",
|
|
6466
|
+
json_body={"role": role, "answers": normalized_answers},
|
|
6467
|
+
)
|
|
6468
|
+
except QingflowApiError as exc:
|
|
6469
|
+
self._remap_record_update_target_context_error(
|
|
6470
|
+
profile,
|
|
6471
|
+
context,
|
|
6472
|
+
app_key=app_key,
|
|
6473
|
+
apply_id=normalized_apply_id,
|
|
6474
|
+
exc=exc,
|
|
6475
|
+
)
|
|
6476
|
+
raise
|
|
6364
6477
|
verification = self._verify_record_write_result(
|
|
6365
6478
|
context,
|
|
6366
6479
|
app_key=app_key,
|
|
@@ -8810,6 +8923,8 @@ class RecordTools(ToolBase):
|
|
|
8810
8923
|
error_payload["request_id"] = parsed.get("request_id")
|
|
8811
8924
|
if isinstance(parsed.get("request_route"), dict):
|
|
8812
8925
|
request_route = cast(JSONObject, parsed.get("request_route"))
|
|
8926
|
+
elif details_payload is not None and isinstance(details_payload.get("request_route"), dict):
|
|
8927
|
+
request_route = cast(JSONObject, details_payload.get("request_route"))
|
|
8813
8928
|
response: JSONObject = {
|
|
8814
8929
|
"profile": profile,
|
|
8815
8930
|
"ws_id": None,
|
|
@@ -96,7 +96,9 @@ class ResourceReadTools(ToolBase):
|
|
|
96
96
|
)
|
|
97
97
|
)
|
|
98
98
|
if system_view is not None:
|
|
99
|
-
export_capability = _view_export_capability_payload(
|
|
99
|
+
export_capability = _view_export_capability_payload(
|
|
100
|
+
supported=_export_supported_for_view_type(system_view["view_type"])
|
|
101
|
+
)
|
|
100
102
|
return self._run(
|
|
101
103
|
profile,
|
|
102
104
|
lambda session_profile, _context: {
|
|
@@ -133,13 +135,11 @@ class ResourceReadTools(ToolBase):
|
|
|
133
135
|
)
|
|
134
136
|
|
|
135
137
|
def runner(session_profile, context):
|
|
136
|
-
|
|
138
|
+
raw_view_type = None
|
|
137
139
|
warnings: list[JSONObject] = []
|
|
138
140
|
verification = {
|
|
139
141
|
"view_exists": True,
|
|
140
142
|
"questions_verified": True,
|
|
141
|
-
"export_route_supported": export_capability["supported"],
|
|
142
|
-
"export_permission_verified": export_capability["permission_verified"],
|
|
143
143
|
}
|
|
144
144
|
config = self.backend.request("GET", context, f"/view/{view_key}/viewConfig")
|
|
145
145
|
base_info = self.backend.request("GET", context, f"/view/{view_key}/viewConfig/baseInfo")
|
|
@@ -161,6 +161,11 @@ class ResourceReadTools(ToolBase):
|
|
|
161
161
|
str(base_info.get("viewgraphType") or "").strip()
|
|
162
162
|
or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
|
|
163
163
|
)
|
|
164
|
+
export_capability = _view_export_capability_payload(
|
|
165
|
+
supported=_export_supported_for_view_type(raw_view_type or None)
|
|
166
|
+
)
|
|
167
|
+
verification["export_route_supported"] = export_capability["supported"]
|
|
168
|
+
verification["export_permission_verified"] = export_capability["permission_verified"]
|
|
164
169
|
resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
|
|
165
170
|
if not resolved_app_key:
|
|
166
171
|
resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
|
|
@@ -176,12 +181,13 @@ class ResourceReadTools(ToolBase):
|
|
|
176
181
|
"message": f"view_get could not resolve app_key for `{view_id}` from view metadata; keep using the app_key from the parent app or portal context.",
|
|
177
182
|
}
|
|
178
183
|
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
184
|
+
if export_capability["supported"]:
|
|
185
|
+
warnings.append(
|
|
186
|
+
{
|
|
187
|
+
"code": "VIEW_EXPORT_APP_CONTEXT_REQUIRED",
|
|
188
|
+
"message": f"view_get supports exporting `{view_id}`, but the export call still needs an explicit `app_key` from the parent app context.",
|
|
189
|
+
}
|
|
190
|
+
)
|
|
185
191
|
return {
|
|
186
192
|
"profile": profile,
|
|
187
193
|
"ws_id": session_profile.selected_ws_id,
|
|
@@ -493,6 +499,10 @@ def _view_export_capability_payload(*, supported: bool) -> JSONObject:
|
|
|
493
499
|
}
|
|
494
500
|
|
|
495
501
|
|
|
502
|
+
def _export_supported_for_view_type(view_type: str | None) -> bool:
|
|
503
|
+
return _analysis_supported_for_view_type(view_type)
|
|
504
|
+
|
|
505
|
+
|
|
496
506
|
def _normalize_view_type(view_type: Any) -> str | None:
|
|
497
507
|
value = str(view_type or "").strip()
|
|
498
508
|
if not value:
|