@qingflow-tech/qingflow-app-builder-mcp 1.0.3 → 1.0.5
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 +2 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +164 -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/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/app.py +47 -1
- package/src/qingflow_mcp/cli/commands/builder.py +7 -0
- package/src/qingflow_mcp/cli/commands/exports.py +111 -0
- package/src/qingflow_mcp/cli/commands/record.py +44 -5
- package/src/qingflow_mcp/cli/commands/task.py +644 -22
- package/src/qingflow_mcp/cli/commands/workspace.py +64 -2
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +240 -5
- package/src/qingflow_mcp/cli/interaction.py +72 -0
- package/src/qingflow_mcp/cli/main.py +5 -0
- package/src/qingflow_mcp/cli/terminal_ui.py +218 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/export_store.py +14 -0
- package/src/qingflow_mcp/public_surface.py +7 -1
- package/src/qingflow_mcp/response_trim.py +188 -10
- package/src/qingflow_mcp/server.py +37 -9
- package/src/qingflow_mcp/server_app_builder.py +4 -0
- package/src/qingflow_mcp/server_app_user.py +115 -10
- package/src/qingflow_mcp/session_store.py +57 -6
- package/src/qingflow_mcp/tools/ai_builder_tools.py +59 -16
- package/src/qingflow_mcp/tools/auth_tools.py +26 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +0 -2
- package/src/qingflow_mcp/tools/export_tools.py +1565 -0
- package/src/qingflow_mcp/tools/import_tools.py +42 -2
- package/src/qingflow_mcp/tools/record_tools.py +12793 -8612
- package/src/qingflow_mcp/tools/resource_read_tools.py +40 -1
- package/src/qingflow_mcp/tools/task_context_tools.py +26 -8
|
@@ -13,6 +13,7 @@ from .tools.app_tools import AppTools
|
|
|
13
13
|
from .tools.auth_tools import AuthTools
|
|
14
14
|
from .tools.code_block_tools import CodeBlockTools
|
|
15
15
|
from .tools.directory_tools import DirectoryTools
|
|
16
|
+
from .tools.export_tools import ExportTools
|
|
16
17
|
from .tools.feedback_tools import FeedbackTools
|
|
17
18
|
from .tools.file_tools import FileTools
|
|
18
19
|
from .tools.import_tools import ImportTools
|
|
@@ -32,7 +33,8 @@ def build_user_server() -> FastMCP:
|
|
|
32
33
|
|
|
33
34
|
If `app_key` is unknown, use `app_list` or `app_search` first.
|
|
34
35
|
If the app is known but the data range is not, use `app_get` first and choose from `accessible_views`.
|
|
35
|
-
If an accessible view has `analysis_supported=false`, do not use it for `
|
|
36
|
+
If an accessible view has `analysis_supported=false`, do not use it for `record_access` or `record_list`. `boardView` and `ganttView` are special UI views, not data-access targets.
|
|
37
|
+
`view_get(view_id=...)` also returns `export_capability`; it only means there is a supported export route, not that export permission has been verified.
|
|
36
38
|
|
|
37
39
|
## Shared Helper
|
|
38
40
|
|
|
@@ -47,7 +49,7 @@ If an accessible view has `analysis_supported=false`, do not use it for `record_
|
|
|
47
49
|
Call `record_insert_schema_get` before `record_insert`.
|
|
48
50
|
Call `record_update_schema_get` before `record_update`.
|
|
49
51
|
Call `record_code_block_schema_get` before `record_code_block_run`.
|
|
50
|
-
Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `
|
|
52
|
+
Call `app_get` first when the data range is unclear, then use `record_browse_schema_get(view_id=...)` before `record_access`, `record_list`, or `record_get`.
|
|
51
53
|
Call `record_import_schema_get` when the import field mapping is unclear before template download or verify.
|
|
52
54
|
|
|
53
55
|
- All `field_id` values must come from the schema response.
|
|
@@ -58,7 +60,9 @@ Call `record_import_schema_get` when the import field mapping is unclear before
|
|
|
58
60
|
`record_insert_schema_get` returns the current user's insert-ready applicant schema; read `required_fields`, `optional_fields`, `runtime_linked_required_fields`, and `payload_template`.
|
|
59
61
|
Inside `optional_fields`, any field with `may_become_required=true` is still writable, but may become required when linked visibility or option-driven runtime rules activate.
|
|
60
62
|
`record_update_schema_get` returns the current record's overall update-ready writable field set across matched accessible views; read `writable_fields` and `payload_template`.
|
|
61
|
-
`record_browse_schema_get(view_id=...)` returns
|
|
63
|
+
`record_browse_schema_get(view_id=...)` returns the same readable fields shown in the selected Qingflow table view header.
|
|
64
|
+
`record_access.fields` / CSV columns and `record_list.columns / where / order_by / query_fields` use that exact same view schema; a missing field means it is not readable in that view.
|
|
65
|
+
`searchQueIds` is a backend full-text search scope, not an output-column/projection mechanism.
|
|
62
66
|
`record_code_block_schema_get` returns code-block-ready schema for exact code block field selection.
|
|
63
67
|
`record_import_schema_get` returns import-ready column metadata.
|
|
64
68
|
|
|
@@ -68,16 +72,19 @@ Inside `optional_fields`, any field with `may_become_required=true` is still wri
|
|
|
68
72
|
|
|
69
73
|
## Analytics Path
|
|
70
74
|
|
|
71
|
-
`app_get -> record_browse_schema_get(view_id=...) ->
|
|
75
|
+
`app_get -> record_browse_schema_get(view_id=...) -> record_access -> Python`
|
|
72
76
|
|
|
73
77
|
Prefer `view_id` entries from `accessible_views` where `analysis_supported=true`.
|
|
74
78
|
|
|
75
|
-
Use
|
|
79
|
+
Use `record_access` to write local CSV shard files, then use Python to compute counts, rankings, ratios, trends, and final conclusions. `record_access` does not return bulk `items`; read `files[].local_path`. CSV columns are readable and field-id anchored, such as `项目状态__field_343283094`, and `fields[]` is the compact metadata source.
|
|
80
|
+
For analysis-style tasks, prefer an explicit time range or business filter. If `record_access.status == "needs_scope"`, do not treat it as a failure; ask for a time/business scope or retry with a user-provided period using `scope.suggested_time_fields` / `scope.recommended_where_examples`. If `record_access.status == "partial"`, read the returned files only as a limited subset and do not give a final full-population conclusion.
|
|
81
|
+
Use `chart_get` only when the user provides a report URL / chart_id or explicitly asks to read an existing report. Do not use QingBI as the default analysis route.
|
|
76
82
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
- `
|
|
80
|
-
- `
|
|
83
|
+
Use this data-access DSL shape:
|
|
84
|
+
|
|
85
|
+
- `columns`: `[{{field_id}}]`
|
|
86
|
+
- `where`: `[{{field_id, op, value}}]`
|
|
87
|
+
- `order_by`: `[{{field_id, direction}}]`
|
|
81
88
|
|
|
82
89
|
Important key rules:
|
|
83
90
|
|
|
@@ -87,6 +94,10 @@ Important key rules:
|
|
|
87
94
|
- Do **not** use `aggregation`
|
|
88
95
|
- Do **not** use `operator`
|
|
89
96
|
|
|
97
|
+
`record_list` is for browsing and sample checks, not final analysis conclusions.
|
|
98
|
+
For fuzzy single-record lookup, use `record_list(query=..., query_fields=[{{field_id}}])` to find candidates, read `lookup.next_action`, and only call `record_get` after one candidate is clear.
|
|
99
|
+
`record_list.query_fields` maps to backend full-text search scope (`searchQueIds`); `record_list.columns` only controls displayed fields.
|
|
100
|
+
|
|
90
101
|
Analysis answers must include concrete numbers. When applicable, include percentages based on the returned totals.
|
|
91
102
|
|
|
92
103
|
## Record CRUD Path
|
|
@@ -100,6 +111,7 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
100
111
|
`portal_get -> view_get -> record_list`
|
|
101
112
|
|
|
102
113
|
- Use `columns` as `[{{field_id}}]`
|
|
114
|
+
- Use `query` plus optional `query_fields` when the user provides fuzzy record-identifying text
|
|
103
115
|
- Use `where` items as `{{field_id, op, value}}`
|
|
104
116
|
- Use `order_by` items as `{{field_id, direction}}`
|
|
105
117
|
- Legacy forms such as bare integer `field_id`, `fieldId`, `operator`, `values`, or `order` may still parse, but they are compatibility-only and not the canonical DSL
|
|
@@ -113,7 +125,10 @@ Analysis answers must include concrete numbers. When applicable, include percent
|
|
|
113
125
|
- `linkage.kind=logic_visibility` means linked visibility or option-driven rules are involved; `linkage.kind=reference_fill` means reference/default matching logic is involved; `linkage.kind=formula_fill` means formula/default auto-fill logic is involved.
|
|
114
126
|
- `record_update_schema_get` exposes the overall writable field set for the record, but not every field combination is guaranteed; `record_update` still needs one single matched accessible view that can cover the payload.
|
|
115
127
|
- `record_delete` deletes by `record_id` or `record_ids`.
|
|
116
|
-
-
|
|
128
|
+
- `record_get` is the single-record frontend detail context tool. It returns detail-page visible fields, one-level relation targets, first-page data/workflow logs, associated views/reports, local readable image assets, local downloadable file assets, unavailable context, and `semantic_context`.
|
|
129
|
+
- Read record images from `record_get.media_assets.items[].local_path` when `readable_by_agent=true`; read attachments/documents/tables from `record_get.file_assets.items[].local_path` and `extraction.text_path` when present. `record_get` follows the frontend storage cookie redirect path for Qingflow attachments, and remote file URLs should not be treated as directly readable.
|
|
130
|
+
- `record_get.columns` are focus hints only; they do not project the detail fields. Read facts from top-level `fields[]`.
|
|
131
|
+
- When readback shape matters after insert or update, prefer `record_get` for human/detail-page context, or `record_list(..., output_profile="normalized")` for batch-shaped normalized rows.
|
|
117
132
|
|
|
118
133
|
- Read relation targets from `record_insert_schema_get` / `record_update_schema_get` relation metadata before preparing relation writes.
|
|
119
134
|
- Member and department fields may be written with natural strings directly on `record_insert` / `record_update`; only fall back to `record_member_candidates` or `record_department_candidates` when the user wants explicit candidate browsing or the write returns ambiguity that needs confirmation.
|
|
@@ -142,6 +157,28 @@ Use `record_code_block_run` when the user wants to execute a form code-block fie
|
|
|
142
157
|
- Do not modify user-uploaded files unless the user explicitly authorizes repair.
|
|
143
158
|
- If repair is authorized, keep the original file and repair a copy, then run `record_import_verify` again before `record_import_start`.
|
|
144
159
|
|
|
160
|
+
## Export Path
|
|
161
|
+
|
|
162
|
+
Use export only when the user explicitly asks to export/download/generate an Excel or export file.
|
|
163
|
+
|
|
164
|
+
`view_get -> record_export_start -> record_export_status_get -> record_export_get`
|
|
165
|
+
|
|
166
|
+
- `record_export_direct` is the one-shot path that starts export, waits, downloads locally, and still returns remote download links.
|
|
167
|
+
- Do not use `record_export_direct` as the default analysis path; use `record_access -> Python` instead.
|
|
168
|
+
- Export v1 supports record views only and follows the same public `view_id` semantics as `record_list`.
|
|
169
|
+
- `record_export_start` / `record_export_direct` support frontend-like row selection:
|
|
170
|
+
- omit `record_ids` to export all rows in the selected view
|
|
171
|
+
- pass `record_ids` to export selected rows only
|
|
172
|
+
- `record_export_start` / `record_export_direct` also support internal query selection:
|
|
173
|
+
- pass `where` to resolve matching `record_id` values first
|
|
174
|
+
- pass `order_by` to keep the internal query and export row order aligned with `record_list`
|
|
175
|
+
- then run native export as selected rows
|
|
176
|
+
- `where/order_by` and `record_ids` are mutually exclusive
|
|
177
|
+
- `record_export_start` / `record_export_direct` also support frontend-like column selection:
|
|
178
|
+
- omit `columns` to export all current-view fields
|
|
179
|
+
- pass `columns` to export only selected fields, preserving the provided order
|
|
180
|
+
- `include_workflow_log=true` maps to the native workflow-log export switch.
|
|
181
|
+
|
|
145
182
|
## Task Workflow Path
|
|
146
183
|
|
|
147
184
|
`task_list -> task_get -> task_action_execute`
|
|
@@ -188,6 +225,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
188
225
|
workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
189
226
|
file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
190
227
|
imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
228
|
+
exports = wrap_trimmed_methods(ExportTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
191
229
|
resources = wrap_trimmed_methods(ResourceReadTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
192
230
|
feedback = FeedbackTools(backend, mcp_side="App User MCP")
|
|
193
231
|
code_block_tools = wrap_trimmed_methods(CodeBlockTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
@@ -218,6 +256,73 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
218
256
|
def auth_logout(profile: str = DEFAULT_PROFILE, forget_persisted: bool = False) -> dict:
|
|
219
257
|
return auth.auth_logout(profile=profile, forget_persisted=forget_persisted)
|
|
220
258
|
|
|
259
|
+
@server.tool()
|
|
260
|
+
def record_export_start(
|
|
261
|
+
profile: str = DEFAULT_PROFILE,
|
|
262
|
+
app_key: str = "",
|
|
263
|
+
view_id: str = "system:all",
|
|
264
|
+
columns: list[dict | int] | None = None,
|
|
265
|
+
where: list[dict] | None = None,
|
|
266
|
+
order_by: list[dict] | None = None,
|
|
267
|
+
record_ids: list[str | int] | None = None,
|
|
268
|
+
include_workflow_log: bool = False,
|
|
269
|
+
) -> dict:
|
|
270
|
+
return exports.record_export_start(
|
|
271
|
+
profile=profile,
|
|
272
|
+
app_key=app_key,
|
|
273
|
+
view_id=view_id,
|
|
274
|
+
columns=columns or [],
|
|
275
|
+
where=where or [],
|
|
276
|
+
order_by=order_by or [],
|
|
277
|
+
record_ids=record_ids or [],
|
|
278
|
+
include_workflow_log=include_workflow_log,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
@server.tool()
|
|
282
|
+
def record_export_status_get(
|
|
283
|
+
profile: str = DEFAULT_PROFILE,
|
|
284
|
+
export_handle: str = "",
|
|
285
|
+
) -> dict:
|
|
286
|
+
return exports.record_export_status_get(profile=profile, export_handle=export_handle)
|
|
287
|
+
|
|
288
|
+
@server.tool()
|
|
289
|
+
def record_export_get(
|
|
290
|
+
profile: str = DEFAULT_PROFILE,
|
|
291
|
+
export_handle: str = "",
|
|
292
|
+
download_to_path: str | None = None,
|
|
293
|
+
) -> dict:
|
|
294
|
+
return exports.record_export_get(
|
|
295
|
+
profile=profile,
|
|
296
|
+
export_handle=export_handle,
|
|
297
|
+
download_to_path=download_to_path,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
@server.tool()
|
|
301
|
+
def record_export_direct(
|
|
302
|
+
profile: str = DEFAULT_PROFILE,
|
|
303
|
+
app_key: str = "",
|
|
304
|
+
view_id: str = "system:all",
|
|
305
|
+
columns: list[dict | int] | None = None,
|
|
306
|
+
where: list[dict] | None = None,
|
|
307
|
+
order_by: list[dict] | None = None,
|
|
308
|
+
record_ids: list[str | int] | None = None,
|
|
309
|
+
include_workflow_log: bool = False,
|
|
310
|
+
download_to_path: str | None = None,
|
|
311
|
+
wait_timeout_seconds: float | None = None,
|
|
312
|
+
) -> dict:
|
|
313
|
+
return exports.record_export_direct(
|
|
314
|
+
profile=profile,
|
|
315
|
+
app_key=app_key,
|
|
316
|
+
view_id=view_id,
|
|
317
|
+
columns=columns or [],
|
|
318
|
+
where=where or [],
|
|
319
|
+
order_by=order_by or [],
|
|
320
|
+
record_ids=record_ids or [],
|
|
321
|
+
include_workflow_log=include_workflow_log,
|
|
322
|
+
download_to_path=download_to_path,
|
|
323
|
+
wait_timeout_seconds=wait_timeout_seconds,
|
|
324
|
+
)
|
|
325
|
+
|
|
221
326
|
@server.tool()
|
|
222
327
|
def workspace_list(
|
|
223
328
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -2,8 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from dataclasses import asdict, dataclass
|
|
6
|
-
from datetime import datetime, timezone
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
try:
|
|
@@ -82,6 +83,11 @@ class SessionStore:
|
|
|
82
83
|
self._keyring = keyring_backend if keyring_backend is not None else keyring
|
|
83
84
|
self._memory_sessions: dict[str, BackendSession] = {}
|
|
84
85
|
self._logged_out_profiles: set[str] = set()
|
|
86
|
+
self._profile_refresher: Callable[[str, SessionProfile | None], bool] | None = None
|
|
87
|
+
self._refreshing_profiles: set[str] = set()
|
|
88
|
+
|
|
89
|
+
def set_profile_refresher(self, refresher: Callable[[str, SessionProfile | None], bool] | None) -> None:
|
|
90
|
+
self._profile_refresher = refresher
|
|
85
91
|
|
|
86
92
|
def save_session(
|
|
87
93
|
self,
|
|
@@ -148,9 +154,14 @@ class SessionStore:
|
|
|
148
154
|
def get_profile(self, profile: str) -> SessionProfile | None:
|
|
149
155
|
payload = self._load_profiles()
|
|
150
156
|
raw_profile = payload.get("profiles", {}).get(profile)
|
|
151
|
-
if
|
|
152
|
-
|
|
153
|
-
|
|
157
|
+
session_profile = SessionProfile.from_dict(raw_profile) if isinstance(raw_profile, dict) else None
|
|
158
|
+
if profile in self._refreshing_profiles:
|
|
159
|
+
return session_profile
|
|
160
|
+
if self._should_refresh_profile(payload, session_profile) and self._refresh_profile(profile, session_profile):
|
|
161
|
+
payload = self._load_profiles()
|
|
162
|
+
raw_profile = payload.get("profiles", {}).get(profile)
|
|
163
|
+
session_profile = SessionProfile.from_dict(raw_profile) if isinstance(raw_profile, dict) else None
|
|
164
|
+
return session_profile
|
|
154
165
|
|
|
155
166
|
def get_backend_session(self, profile: str) -> BackendSession | None:
|
|
156
167
|
if profile in self._logged_out_profiles:
|
|
@@ -283,8 +294,48 @@ class SessionStore:
|
|
|
283
294
|
def _load_profiles(self) -> JSONObject:
|
|
284
295
|
if not self._profiles_path.exists():
|
|
285
296
|
return {"profiles": {}}
|
|
286
|
-
|
|
287
|
-
|
|
297
|
+
try:
|
|
298
|
+
with self._profiles_path.open("r", encoding="utf-8") as handle:
|
|
299
|
+
payload = json.load(handle)
|
|
300
|
+
except (OSError, json.JSONDecodeError):
|
|
301
|
+
return {"profiles": {}}
|
|
302
|
+
if not isinstance(payload, dict):
|
|
303
|
+
return {"profiles": {}}
|
|
304
|
+
profiles = payload.get("profiles")
|
|
305
|
+
if not isinstance(profiles, dict):
|
|
306
|
+
payload["profiles"] = {}
|
|
307
|
+
return payload
|
|
308
|
+
|
|
309
|
+
def _should_refresh_profile(self, payload: JSONObject, session_profile: SessionProfile | None) -> bool:
|
|
310
|
+
if self._profile_refresher is None:
|
|
311
|
+
return False
|
|
312
|
+
profiles = payload.get("profiles")
|
|
313
|
+
if not isinstance(profiles, dict) or not profiles:
|
|
314
|
+
return True
|
|
315
|
+
if session_profile is None:
|
|
316
|
+
return True
|
|
317
|
+
return self._is_profile_stale(session_profile)
|
|
318
|
+
|
|
319
|
+
def _refresh_profile(self, profile: str, session_profile: SessionProfile | None) -> bool:
|
|
320
|
+
if self._profile_refresher is None:
|
|
321
|
+
return False
|
|
322
|
+
self._refreshing_profiles.add(profile)
|
|
323
|
+
try:
|
|
324
|
+
return bool(self._profile_refresher(profile, session_profile))
|
|
325
|
+
except Exception:
|
|
326
|
+
return False
|
|
327
|
+
finally:
|
|
328
|
+
self._refreshing_profiles.discard(profile)
|
|
329
|
+
|
|
330
|
+
def _is_profile_stale(self, session_profile: SessionProfile) -> bool:
|
|
331
|
+
timestamp_text = session_profile.updated_at or session_profile.created_at
|
|
332
|
+
try:
|
|
333
|
+
timestamp = datetime.fromisoformat(timestamp_text)
|
|
334
|
+
except (TypeError, ValueError):
|
|
335
|
+
return True
|
|
336
|
+
if timestamp.tzinfo is None:
|
|
337
|
+
timestamp = timestamp.replace(tzinfo=timezone.utc)
|
|
338
|
+
return datetime.now(timezone.utc) - timestamp > timedelta(days=7)
|
|
288
339
|
|
|
289
340
|
def _save_profiles(self, payload: JSONObject) -> None:
|
|
290
341
|
self._profiles_path.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -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
|
},
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Any
|
|
5
|
+
import os
|
|
5
6
|
|
|
6
7
|
from mcp.server.fastmcp import FastMCP
|
|
7
8
|
|
|
@@ -30,6 +31,7 @@ class AuthTools(ToolBase):
|
|
|
30
31
|
def __init__(self, sessions: SessionStore, backend) -> None:
|
|
31
32
|
"""执行内部辅助逻辑。"""
|
|
32
33
|
super().__init__(sessions, backend)
|
|
34
|
+
self.sessions.set_profile_refresher(self._refresh_profile_via_credential)
|
|
33
35
|
|
|
34
36
|
def register(self, mcp: FastMCP) -> None:
|
|
35
37
|
"""注册当前工具到 MCP 服务。"""
|
|
@@ -314,8 +316,12 @@ class AuthTools(ToolBase):
|
|
|
314
316
|
mcporter_context = self._read_mcporter_qingflow_context()
|
|
315
317
|
if not normalized_base_url:
|
|
316
318
|
normalized_base_url = self._normalize_text(mcporter_context.get("base_url"))
|
|
319
|
+
if not normalized_base_url:
|
|
320
|
+
normalized_base_url = os.getenv("QINGFLOW_BASE_URL")
|
|
317
321
|
if not normalized_credential:
|
|
318
322
|
normalized_credential = self._normalize_text(mcporter_context.get("credential"))
|
|
323
|
+
if not normalized_credential:
|
|
324
|
+
normalized_credential = os.getenv("QINGFLOW_CREDENTIAL")
|
|
319
325
|
return normalized_base_url, normalized_credential or ""
|
|
320
326
|
|
|
321
327
|
def _read_mcporter_qingflow_context(self) -> dict[str, str]:
|
|
@@ -344,6 +350,26 @@ class AuthTools(ToolBase):
|
|
|
344
350
|
"credential": str(credential or "").strip(),
|
|
345
351
|
}
|
|
346
352
|
|
|
353
|
+
def _refresh_profile_via_credential(
|
|
354
|
+
self,
|
|
355
|
+
profile: str,
|
|
356
|
+
session_profile,
|
|
357
|
+
) -> bool:
|
|
358
|
+
backend_session = self.sessions.get_backend_session(profile) if session_profile is not None else None
|
|
359
|
+
base_url = session_profile.base_url if session_profile is not None else None
|
|
360
|
+
credential = backend_session.credential if backend_session is not None else None
|
|
361
|
+
persist = bool(session_profile.persisted) if session_profile is not None else False
|
|
362
|
+
try:
|
|
363
|
+
self.auth_use_credential(
|
|
364
|
+
profile=profile,
|
|
365
|
+
base_url=base_url,
|
|
366
|
+
credential=credential,
|
|
367
|
+
persist=persist,
|
|
368
|
+
)
|
|
369
|
+
except (QingflowApiError, RuntimeError):
|
|
370
|
+
return False
|
|
371
|
+
return True
|
|
372
|
+
|
|
347
373
|
@tool_cn_name("我的身份")
|
|
348
374
|
def auth_whoami(self, *, profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
|
|
349
375
|
"""执行认证与会话相关逻辑。"""
|
|
@@ -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"),
|