@josephyan/qingflow-cli 0.2.0-beta.69 → 0.2.0-beta.70
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/backend_client.py +0 -1
- package/src/qingflow_mcp/builder_facade/models.py +34 -0
- package/src/qingflow_mcp/builder_facade/service.py +337 -17
- package/src/qingflow_mcp/cli/commands/builder.py +248 -1
- package/src/qingflow_mcp/cli/commands/common.py +15 -0
- package/src/qingflow_mcp/cli/commands/imports.py +12 -2
- package/src/qingflow_mcp/cli/commands/record.py +132 -32
- package/src/qingflow_mcp/cli/commands/workspace.py +1 -1
- package/src/qingflow_mcp/cli/formatters.py +52 -2
- package/src/qingflow_mcp/cli/main.py +7 -5
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server_app_builder.py +136 -8
- package/src/qingflow_mcp/server_app_user.py +55 -11
- package/src/qingflow_mcp/tools/ai_builder_tools.py +270 -5
- package/src/qingflow_mcp/tools/app_tools.py +29 -0
- package/src/qingflow_mcp/tools/auth_tools.py +259 -1
- package/src/qingflow_mcp/tools/import_tools.py +59 -7
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +23 -6
- package/src/qingflow_mcp/tools/record_tools.py +6 -12
- package/src/qingflow_mcp/tools/workspace_tools.py +124 -7
|
@@ -36,7 +36,7 @@ from ..builder_facade.models import (
|
|
|
36
36
|
ViewsPreset,
|
|
37
37
|
ViewsPlanRequest,
|
|
38
38
|
)
|
|
39
|
-
from ..builder_facade.service import AiBuilderFacade
|
|
39
|
+
from ..builder_facade.service import AiBuilderFacade, INTEGRATION_OUTPUT_TARGET_FIELD_TYPES
|
|
40
40
|
from .app_tools import AppTools
|
|
41
41
|
from .base import ToolBase
|
|
42
42
|
from .custom_button_tools import CustomButtonTools
|
|
@@ -138,9 +138,8 @@ class AiBuilderTools(ToolBase):
|
|
|
138
138
|
profile: str = DEFAULT_PROFILE,
|
|
139
139
|
tag_id: int = 0,
|
|
140
140
|
app_key: str = "",
|
|
141
|
-
app_title: str = "",
|
|
142
141
|
) -> JSONObject:
|
|
143
|
-
return self.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key
|
|
142
|
+
return self.package_attach_app(profile=profile, tag_id=tag_id, app_key=app_key)
|
|
144
143
|
|
|
145
144
|
@mcp.tool()
|
|
146
145
|
def app_release_edit_lock_if_mine(
|
|
@@ -163,6 +162,21 @@ class AiBuilderTools(ToolBase):
|
|
|
163
162
|
app_name: str = "",
|
|
164
163
|
package_tag_id: int | None = None,
|
|
165
164
|
) -> JSONObject:
|
|
165
|
+
has_app_key = bool((app_key or "").strip())
|
|
166
|
+
has_app_name = bool((app_name or "").strip())
|
|
167
|
+
has_package_tag_id = package_tag_id is not None
|
|
168
|
+
if has_app_key and (has_app_name or has_package_tag_id):
|
|
169
|
+
return _config_failure(
|
|
170
|
+
tool_name="app_resolve",
|
|
171
|
+
message="app_resolve accepts exactly one selector mode.",
|
|
172
|
+
fix_hint="Use only `app_key`, or use `app_name` together with `package_tag_id`.",
|
|
173
|
+
)
|
|
174
|
+
if not has_app_key and not (has_app_name and has_package_tag_id):
|
|
175
|
+
return _config_failure(
|
|
176
|
+
tool_name="app_resolve",
|
|
177
|
+
message="app_resolve requires either app_key, or app_name together with package_tag_id.",
|
|
178
|
+
fix_hint="For an existing known app, pass `app_key`. For package-scoped lookup, pass both `app_name` and `package_tag_id`.",
|
|
179
|
+
)
|
|
166
180
|
return self.app_resolve(profile=profile, app_key=app_key, app_name=app_name, package_tag_id=package_tag_id)
|
|
167
181
|
|
|
168
182
|
@mcp.tool()
|
|
@@ -218,6 +232,18 @@ class AiBuilderTools(ToolBase):
|
|
|
218
232
|
def app_read_charts_summary(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
219
233
|
return self.app_read_charts_summary(profile=profile, app_key=app_key)
|
|
220
234
|
|
|
235
|
+
@mcp.tool()
|
|
236
|
+
def portal_list(profile: str = DEFAULT_PROFILE) -> JSONObject:
|
|
237
|
+
return self.portal_list(profile=profile)
|
|
238
|
+
|
|
239
|
+
@mcp.tool()
|
|
240
|
+
def portal_get(
|
|
241
|
+
profile: str = DEFAULT_PROFILE,
|
|
242
|
+
dash_key: str = "",
|
|
243
|
+
being_draft: bool = True,
|
|
244
|
+
) -> JSONObject:
|
|
245
|
+
return self.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
246
|
+
|
|
221
247
|
@mcp.tool()
|
|
222
248
|
def portal_read_summary(
|
|
223
249
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -226,6 +252,30 @@ class AiBuilderTools(ToolBase):
|
|
|
226
252
|
) -> JSONObject:
|
|
227
253
|
return self.portal_read_summary(profile=profile, dash_key=dash_key, being_draft=being_draft)
|
|
228
254
|
|
|
255
|
+
@mcp.tool()
|
|
256
|
+
def view_get(profile: str = DEFAULT_PROFILE, viewgraph_key: str = "") -> JSONObject:
|
|
257
|
+
return self.view_get(profile=profile, viewgraph_key=viewgraph_key)
|
|
258
|
+
|
|
259
|
+
@mcp.tool()
|
|
260
|
+
def chart_get(
|
|
261
|
+
profile: str = DEFAULT_PROFILE,
|
|
262
|
+
chart_id: str = "",
|
|
263
|
+
data_payload: JSONObject | None = None,
|
|
264
|
+
page_num: int | None = None,
|
|
265
|
+
page_size: int | None = None,
|
|
266
|
+
page_num_y: int | None = None,
|
|
267
|
+
page_size_y: int | None = None,
|
|
268
|
+
) -> JSONObject:
|
|
269
|
+
return self.chart_get(
|
|
270
|
+
profile=profile,
|
|
271
|
+
chart_id=chart_id,
|
|
272
|
+
data_payload=data_payload or {},
|
|
273
|
+
page_num=page_num,
|
|
274
|
+
page_size=page_size,
|
|
275
|
+
page_num_y=page_num_y,
|
|
276
|
+
page_size_y=page_size_y,
|
|
277
|
+
)
|
|
278
|
+
|
|
229
279
|
@mcp.tool()
|
|
230
280
|
def app_schema_apply(
|
|
231
281
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -241,6 +291,23 @@ class AiBuilderTools(ToolBase):
|
|
|
241
291
|
update_fields: list[JSONObject] | None = None,
|
|
242
292
|
remove_fields: list[JSONObject] | None = None,
|
|
243
293
|
) -> JSONObject:
|
|
294
|
+
has_app_key = bool((app_key or "").strip())
|
|
295
|
+
has_app_name = bool((app_name or "").strip())
|
|
296
|
+
has_app_title = bool((app_title or "").strip())
|
|
297
|
+
has_package_tag_id = package_tag_id is not None
|
|
298
|
+
if has_app_key:
|
|
299
|
+
if create_if_missing or has_app_name or has_package_tag_id or has_app_title:
|
|
300
|
+
return _config_failure(
|
|
301
|
+
tool_name="app_schema_apply",
|
|
302
|
+
message="app_schema_apply edit mode only accepts app_key as the resource selector.",
|
|
303
|
+
fix_hint="For existing apps, pass `app_key` only. For create mode, use `package_tag_id + app_name + create_if_missing=true`.",
|
|
304
|
+
)
|
|
305
|
+
elif not (create_if_missing and has_package_tag_id and has_app_name):
|
|
306
|
+
return _config_failure(
|
|
307
|
+
tool_name="app_schema_apply",
|
|
308
|
+
message="app_schema_apply create mode requires package_tag_id, app_name, and create_if_missing=true.",
|
|
309
|
+
fix_hint="Use `app_key` for existing apps, or pass `package_tag_id + app_name + create_if_missing=true` to create a new app.",
|
|
310
|
+
)
|
|
244
311
|
return self.app_schema_apply(
|
|
245
312
|
profile=profile,
|
|
246
313
|
app_key=app_key,
|
|
@@ -331,6 +398,21 @@ class AiBuilderTools(ToolBase):
|
|
|
331
398
|
dash_global_config: JSONObject | None = None,
|
|
332
399
|
config: JSONObject | None = None,
|
|
333
400
|
) -> JSONObject:
|
|
401
|
+
has_dash_key = bool((dash_key or "").strip())
|
|
402
|
+
has_dash_name = bool((dash_name or "").strip())
|
|
403
|
+
has_package_tag_id = package_tag_id is not None
|
|
404
|
+
if has_dash_key and has_package_tag_id:
|
|
405
|
+
return _config_failure(
|
|
406
|
+
tool_name="portal_apply",
|
|
407
|
+
message="portal_apply accepts exactly one selector mode.",
|
|
408
|
+
fix_hint="Use `dash_key` to update an existing portal, or use `package_tag_id + dash_name` to create a new portal.",
|
|
409
|
+
)
|
|
410
|
+
if not has_dash_key and not (has_package_tag_id and has_dash_name):
|
|
411
|
+
return _config_failure(
|
|
412
|
+
tool_name="portal_apply",
|
|
413
|
+
message="portal_apply requires either dash_key, or package_tag_id together with dash_name.",
|
|
414
|
+
fix_hint="Use `dash_key` for an existing portal. For create mode, pass `package_tag_id + dash_name`.",
|
|
415
|
+
)
|
|
334
416
|
return self.portal_apply(
|
|
335
417
|
profile=profile,
|
|
336
418
|
dash_key=dash_key,
|
|
@@ -725,6 +807,23 @@ class AiBuilderTools(ToolBase):
|
|
|
725
807
|
suggested_next_call={"tool_name": "app_read_charts_summary", "arguments": {"profile": profile, "app_key": app_key}},
|
|
726
808
|
)
|
|
727
809
|
|
|
810
|
+
def portal_list(self, *, profile: str) -> JSONObject:
|
|
811
|
+
return _safe_tool_call(
|
|
812
|
+
lambda: self._facade.portal_list(profile=profile),
|
|
813
|
+
error_code="PORTAL_LIST_FAILED",
|
|
814
|
+
normalized_args={},
|
|
815
|
+
suggested_next_call={"tool_name": "portal_list", "arguments": {"profile": profile}},
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
def portal_get(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
|
|
819
|
+
normalized_args = {"dash_key": dash_key, "being_draft": being_draft}
|
|
820
|
+
return _safe_tool_call(
|
|
821
|
+
lambda: self._facade.portal_get(profile=profile, dash_key=dash_key, being_draft=being_draft),
|
|
822
|
+
error_code="PORTAL_GET_FAILED",
|
|
823
|
+
normalized_args=normalized_args,
|
|
824
|
+
suggested_next_call={"tool_name": "portal_get", "arguments": {"profile": profile, **normalized_args}},
|
|
825
|
+
)
|
|
826
|
+
|
|
728
827
|
def portal_read_summary(self, *, profile: str, dash_key: str, being_draft: bool = True) -> JSONObject:
|
|
729
828
|
normalized_args = {"dash_key": dash_key, "being_draft": being_draft}
|
|
730
829
|
return _safe_tool_call(
|
|
@@ -734,6 +833,49 @@ class AiBuilderTools(ToolBase):
|
|
|
734
833
|
suggested_next_call={"tool_name": "portal_read_summary", "arguments": {"profile": profile, **normalized_args}},
|
|
735
834
|
)
|
|
736
835
|
|
|
836
|
+
def view_get(self, *, profile: str, viewgraph_key: str) -> JSONObject:
|
|
837
|
+
normalized_args = {"viewgraph_key": viewgraph_key}
|
|
838
|
+
return _safe_tool_call(
|
|
839
|
+
lambda: self._facade.view_get(profile=profile, viewgraph_key=viewgraph_key),
|
|
840
|
+
error_code="VIEW_GET_FAILED",
|
|
841
|
+
normalized_args=normalized_args,
|
|
842
|
+
suggested_next_call={"tool_name": "view_get", "arguments": {"profile": profile, **normalized_args}},
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
def chart_get(
|
|
846
|
+
self,
|
|
847
|
+
*,
|
|
848
|
+
profile: str,
|
|
849
|
+
chart_id: str,
|
|
850
|
+
data_payload: JSONObject | None = None,
|
|
851
|
+
page_num: int | None = None,
|
|
852
|
+
page_size: int | None = None,
|
|
853
|
+
page_num_y: int | None = None,
|
|
854
|
+
page_size_y: int | None = None,
|
|
855
|
+
) -> JSONObject:
|
|
856
|
+
normalized_args = {
|
|
857
|
+
"chart_id": chart_id,
|
|
858
|
+
"data_payload": deepcopy(data_payload) if isinstance(data_payload, dict) else {},
|
|
859
|
+
"page_num": page_num,
|
|
860
|
+
"page_size": page_size,
|
|
861
|
+
"page_num_y": page_num_y,
|
|
862
|
+
"page_size_y": page_size_y,
|
|
863
|
+
}
|
|
864
|
+
return _safe_tool_call(
|
|
865
|
+
lambda: self._facade.chart_get(
|
|
866
|
+
profile=profile,
|
|
867
|
+
chart_id=chart_id,
|
|
868
|
+
data_payload=normalized_args["data_payload"],
|
|
869
|
+
page_num=page_num,
|
|
870
|
+
page_size=page_size,
|
|
871
|
+
page_num_y=page_num_y,
|
|
872
|
+
page_size_y=page_size_y,
|
|
873
|
+
),
|
|
874
|
+
error_code="CHART_GET_FAILED",
|
|
875
|
+
normalized_args=normalized_args,
|
|
876
|
+
suggested_next_call={"tool_name": "chart_get", "arguments": {"profile": profile, "chart_id": chart_id}},
|
|
877
|
+
)
|
|
878
|
+
|
|
737
879
|
def app_schema_plan(
|
|
738
880
|
self,
|
|
739
881
|
*,
|
|
@@ -1645,6 +1787,29 @@ def _validation_failure(
|
|
|
1645
1787
|
}
|
|
1646
1788
|
|
|
1647
1789
|
|
|
1790
|
+
def _config_failure(*, tool_name: str, message: str, fix_hint: str) -> JSONObject:
|
|
1791
|
+
contract = _BUILDER_TOOL_CONTRACTS.get(tool_name or "")
|
|
1792
|
+
return {
|
|
1793
|
+
"status": "failed",
|
|
1794
|
+
"error_code": "CONFIG_ERROR",
|
|
1795
|
+
"recoverable": True,
|
|
1796
|
+
"message": message,
|
|
1797
|
+
"normalized_args": {},
|
|
1798
|
+
"missing_fields": [],
|
|
1799
|
+
"allowed_values": deepcopy(contract.get("allowed_values", {})) if isinstance(contract, dict) else {},
|
|
1800
|
+
"details": {
|
|
1801
|
+
"fix_hint": fix_hint,
|
|
1802
|
+
"allowed_keys": deepcopy(contract.get("allowed_keys", [])) if isinstance(contract, dict) else [],
|
|
1803
|
+
},
|
|
1804
|
+
"suggested_next_call": None,
|
|
1805
|
+
"request_id": None,
|
|
1806
|
+
"backend_code": None,
|
|
1807
|
+
"http_status": None,
|
|
1808
|
+
"noop": False,
|
|
1809
|
+
"verification": {},
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
|
|
1648
1813
|
def _safe_tool_call(
|
|
1649
1814
|
call,
|
|
1650
1815
|
*,
|
|
@@ -1807,6 +1972,39 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
1807
1972
|
"role_icon": "ex-user-outlined",
|
|
1808
1973
|
},
|
|
1809
1974
|
},
|
|
1975
|
+
"package_attach_app": {
|
|
1976
|
+
"allowed_keys": ["tag_id", "app_key"],
|
|
1977
|
+
"aliases": {},
|
|
1978
|
+
"allowed_values": {},
|
|
1979
|
+
"execution_notes": [
|
|
1980
|
+
"attach one existing app to one existing package",
|
|
1981
|
+
"app_title is no longer accepted as a public selector; resolve the app first and pass app_key",
|
|
1982
|
+
],
|
|
1983
|
+
"minimal_example": {
|
|
1984
|
+
"profile": "default",
|
|
1985
|
+
"tag_id": 1001,
|
|
1986
|
+
"app_key": "APP_KEY",
|
|
1987
|
+
},
|
|
1988
|
+
},
|
|
1989
|
+
"app_resolve": {
|
|
1990
|
+
"allowed_keys": ["app_key", "app_name", "package_tag_id"],
|
|
1991
|
+
"aliases": {},
|
|
1992
|
+
"allowed_values": {},
|
|
1993
|
+
"execution_notes": [
|
|
1994
|
+
"use exactly one selector mode",
|
|
1995
|
+
"mode 1: app_key",
|
|
1996
|
+
"mode 2: app_name + package_tag_id",
|
|
1997
|
+
],
|
|
1998
|
+
"minimal_example": {
|
|
1999
|
+
"profile": "default",
|
|
2000
|
+
"app_key": "APP_KEY",
|
|
2001
|
+
},
|
|
2002
|
+
"package_scoped_example": {
|
|
2003
|
+
"profile": "default",
|
|
2004
|
+
"app_name": "研发项目管理",
|
|
2005
|
+
"package_tag_id": 1001,
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
1810
2008
|
"app_custom_button_list": {
|
|
1811
2009
|
"allowed_keys": ["app_key"],
|
|
1812
2010
|
"aliases": {},
|
|
@@ -1986,9 +2184,14 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
1986
2184
|
"allowed_values": {
|
|
1987
2185
|
"field.type": [member.value for member in PublicFieldType],
|
|
1988
2186
|
"field.relation_mode": [member.value for member in PublicRelationMode],
|
|
2187
|
+
"field.code_block_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
|
|
2188
|
+
"field.q_linker_binding.outputs.target_field.type": list(INTEGRATION_OUTPUT_TARGET_FIELD_TYPES),
|
|
1989
2189
|
"field_type_ids": sorted(FIELD_TYPE_ID_ALIASES.keys()),
|
|
1990
2190
|
},
|
|
1991
2191
|
"execution_notes": [
|
|
2192
|
+
"use exactly one resource mode",
|
|
2193
|
+
"edit mode: app_key",
|
|
2194
|
+
"create mode: package_tag_id + app_name + create_if_missing=true",
|
|
1992
2195
|
"multiple relation fields are backend-risky; read verification.relation_field_limit_verified and warnings before declaring the schema stable",
|
|
1993
2196
|
"backend 49614 is normalized to MULTIPLE_RELATION_FIELDS_UNSUPPORTED with a workaround message",
|
|
1994
2197
|
"relation_mode=multiple maps to referenceConfig.optionalDataNum=0",
|
|
@@ -1996,6 +2199,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
1996
2199
|
"q_linker_binding lets you declare request config, dynamic inputs, alias parsing, and target-field bindings in one step; builder writes remoteLookupConfig plus the existing backend relation-default and questionRelations structures",
|
|
1997
2200
|
"code_block_binding lets you declare inputs, code, alias parsing, and target-field bindings in one step; builder writes codeBlockConfig plus the existing backend relation-default and questionRelations structures",
|
|
1998
2201
|
"builder configures code blocks only; it does not execute or trigger code blocks",
|
|
2202
|
+
"code_block_binding and q_linker_binding target fields are limited to text, long_text, number, amount, date, datetime, single_select, multi_select, and boolean",
|
|
1999
2203
|
],
|
|
2000
2204
|
"minimal_example": {
|
|
2001
2205
|
"profile": "default",
|
|
@@ -2031,6 +2235,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2031
2235
|
"app_key": "APP_SCRIPT",
|
|
2032
2236
|
"publish": True,
|
|
2033
2237
|
"add_fields": [
|
|
2238
|
+
{"name": "客户等级", "type": "text"},
|
|
2034
2239
|
{
|
|
2035
2240
|
"name": "查询代码块",
|
|
2036
2241
|
"type": "code_block",
|
|
@@ -2039,12 +2244,12 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2039
2244
|
{"field": {"name": "客户名称"}, "var": "customerName"},
|
|
2040
2245
|
{"field": {"name": "预算金额"}, "var": "budget"},
|
|
2041
2246
|
],
|
|
2042
|
-
"code": "
|
|
2247
|
+
"code": "qf_output = {}; qf_output.customerLevel = budget > 100000 ? 'A' : 'B';",
|
|
2043
2248
|
"auto_trigger": True,
|
|
2044
2249
|
"custom_button_text_enabled": True,
|
|
2045
2250
|
"custom_button_text": "评估客户",
|
|
2046
2251
|
"outputs": [
|
|
2047
|
-
{"alias": "customerLevel", "path": "$.customerLevel"},
|
|
2252
|
+
{"alias": "customerLevel", "path": "$.customerLevel", "target_field": {"name": "客户等级"}},
|
|
2048
2253
|
],
|
|
2049
2254
|
},
|
|
2050
2255
|
}
|
|
@@ -2355,6 +2560,34 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2355
2560
|
"app_key": "APP_KEY",
|
|
2356
2561
|
},
|
|
2357
2562
|
},
|
|
2563
|
+
"portal_list": {
|
|
2564
|
+
"allowed_keys": [],
|
|
2565
|
+
"aliases": {},
|
|
2566
|
+
"allowed_values": {},
|
|
2567
|
+
"execution_notes": [
|
|
2568
|
+
"returns the current user's accessible portal list",
|
|
2569
|
+
"use this as the portal discovery path before portal_get",
|
|
2570
|
+
"results are compact list items, not raw dash payloads",
|
|
2571
|
+
],
|
|
2572
|
+
"minimal_example": {
|
|
2573
|
+
"profile": "default",
|
|
2574
|
+
},
|
|
2575
|
+
},
|
|
2576
|
+
"portal_get": {
|
|
2577
|
+
"allowed_keys": ["dash_key", "being_draft"],
|
|
2578
|
+
"aliases": {"beingDraft": "being_draft"},
|
|
2579
|
+
"allowed_values": {},
|
|
2580
|
+
"execution_notes": [
|
|
2581
|
+
"returns portal-level detail plus a component inventory",
|
|
2582
|
+
"chart and view components are returned as refs only; use chart_get or view_get for more detail",
|
|
2583
|
+
"being_draft=true reads the current draft view; being_draft=false reads live",
|
|
2584
|
+
],
|
|
2585
|
+
"minimal_example": {
|
|
2586
|
+
"profile": "default",
|
|
2587
|
+
"dash_key": "DASH_KEY",
|
|
2588
|
+
"being_draft": True,
|
|
2589
|
+
},
|
|
2590
|
+
},
|
|
2358
2591
|
"app_charts_apply": {
|
|
2359
2592
|
"allowed_keys": ["app_key", "upsert_charts", "remove_chart_ids", "reorder_chart_ids"],
|
|
2360
2593
|
"aliases": {
|
|
@@ -2419,6 +2652,35 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2419
2652
|
"being_draft": True,
|
|
2420
2653
|
},
|
|
2421
2654
|
},
|
|
2655
|
+
"view_get": {
|
|
2656
|
+
"allowed_keys": ["viewgraph_key"],
|
|
2657
|
+
"aliases": {"viewKey": "viewgraph_key"},
|
|
2658
|
+
"allowed_values": {},
|
|
2659
|
+
"execution_notes": [
|
|
2660
|
+
"returns one view's definition detail",
|
|
2661
|
+
"does not return record data; use record_list with app_key + view_id for rows",
|
|
2662
|
+
"use this after portal_get when a component references a view_ref.view_key",
|
|
2663
|
+
],
|
|
2664
|
+
"minimal_example": {
|
|
2665
|
+
"profile": "default",
|
|
2666
|
+
"viewgraph_key": "VIEW_KEY",
|
|
2667
|
+
},
|
|
2668
|
+
},
|
|
2669
|
+
"chart_get": {
|
|
2670
|
+
"allowed_keys": ["chart_id", "data_payload", "page_num", "page_size", "page_num_y", "page_size_y"],
|
|
2671
|
+
"aliases": {"payload": "data_payload"},
|
|
2672
|
+
"allowed_values": {},
|
|
2673
|
+
"execution_notes": [
|
|
2674
|
+
"returns chart base info, chart config, and chart data together",
|
|
2675
|
+
"chart_id is required; chart names are not accepted here",
|
|
2676
|
+
"data_payload defaults to {} so chart_get queries concrete chart data by default",
|
|
2677
|
+
],
|
|
2678
|
+
"minimal_example": {
|
|
2679
|
+
"profile": "default",
|
|
2680
|
+
"chart_id": "CHART_ID",
|
|
2681
|
+
"data_payload": {},
|
|
2682
|
+
},
|
|
2683
|
+
},
|
|
2422
2684
|
"portal_apply": {
|
|
2423
2685
|
"allowed_keys": ["dash_key", "dash_name", "package_tag_id", "publish", "sections", "auth", "icon", "color", "hide_copyright", "dash_global_config", "config"],
|
|
2424
2686
|
"aliases": {
|
|
@@ -2436,6 +2698,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2436
2698
|
},
|
|
2437
2699
|
"allowed_values": {"section.source_type": ["chart", "view", "grid", "filter", "text", "link"]},
|
|
2438
2700
|
"execution_notes": [
|
|
2701
|
+
"use exactly one resource mode",
|
|
2702
|
+
"update mode: dash_key",
|
|
2703
|
+
"create mode: package_tag_id + dash_name",
|
|
2439
2704
|
"portal_apply uses replace semantics for sections",
|
|
2440
2705
|
"remove a section by omitting it from the new sections list",
|
|
2441
2706
|
"package_tag_id is required when creating a new portal",
|
|
@@ -179,6 +179,8 @@ class AppTools(ToolBase):
|
|
|
179
179
|
accessible_views.extend(self._resolve_accessible_custom_views(context, app_key))
|
|
180
180
|
import_capability, import_warnings = _derive_import_capability(base_info)
|
|
181
181
|
warnings.extend(import_warnings)
|
|
182
|
+
editability, editability_warnings = _derive_editability(base_info)
|
|
183
|
+
warnings.extend(editability_warnings)
|
|
182
184
|
|
|
183
185
|
return {
|
|
184
186
|
"profile": profile,
|
|
@@ -191,6 +193,7 @@ class AppTools(ToolBase):
|
|
|
191
193
|
"app_name": app_name,
|
|
192
194
|
"can_create": can_create,
|
|
193
195
|
"import_capability": import_capability,
|
|
196
|
+
"editability": editability,
|
|
194
197
|
"accessible_views": accessible_views,
|
|
195
198
|
},
|
|
196
199
|
}
|
|
@@ -836,6 +839,32 @@ def _unknown_import_capability(
|
|
|
836
839
|
}
|
|
837
840
|
|
|
838
841
|
|
|
842
|
+
def _derive_editability(base_info: Any) -> tuple[JSONObject, list[JSONObject]]:
|
|
843
|
+
warnings: list[JSONObject] = []
|
|
844
|
+
if not isinstance(base_info, dict):
|
|
845
|
+
warnings.append(
|
|
846
|
+
{
|
|
847
|
+
"code": "APP_EDITABILITY_UNAVAILABLE",
|
|
848
|
+
"message": "app_get could not determine editability because baseInfo was unavailable.",
|
|
849
|
+
}
|
|
850
|
+
)
|
|
851
|
+
return {
|
|
852
|
+
"can_edit_form": None,
|
|
853
|
+
"can_edit_flow": None,
|
|
854
|
+
"can_edit_views": None,
|
|
855
|
+
"can_edit_charts": None,
|
|
856
|
+
}, warnings
|
|
857
|
+
|
|
858
|
+
edit_item_status = _coerce_optional_bool(base_info.get("editItemStatus"))
|
|
859
|
+
data_manage_status = _coerce_optional_bool(base_info.get("dataManageStatus"))
|
|
860
|
+
return {
|
|
861
|
+
"can_edit_form": edit_item_status,
|
|
862
|
+
"can_edit_flow": edit_item_status,
|
|
863
|
+
"can_edit_views": data_manage_status,
|
|
864
|
+
"can_edit_charts": data_manage_status,
|
|
865
|
+
}, warnings
|
|
866
|
+
|
|
867
|
+
|
|
839
868
|
def _coerce_positive_int(value: Any) -> int | None:
|
|
840
869
|
try:
|
|
841
870
|
number = int(value)
|