@qingflow-tech/qingflow-app-user-mcp 1.0.2 → 1.0.3
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/docs/local-agent-install.md +9 -3
- package/npm/lib/runtime.mjs +10 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-user/SKILL.md +21 -12
- package/skills/qingflow-app-user/references/data-gotchas.md +1 -1
- package/skills/qingflow-app-user/references/public-surface-sync.md +70 -0
- package/skills/qingflow-app-user/references/record-patterns.md +1 -1
- package/skills/qingflow-record-analysis/SKILL.md +44 -2
- package/skills/qingflow-record-insert/SKILL.md +3 -0
- package/skills/qingflow-record-update/SKILL.md +3 -0
- package/skills/qingflow-task-ops/SKILL.md +31 -10
- package/src/qingflow_mcp/__init__.py +33 -1
- package/src/qingflow_mcp/builder_facade/models.py +14 -4
- package/src/qingflow_mcp/builder_facade/service.py +1582 -124
- package/src/qingflow_mcp/cli/commands/auth.py +63 -0
- package/src/qingflow_mcp/cli/commands/builder.py +4 -3
- package/src/qingflow_mcp/cli/commands/record.py +5 -5
- package/src/qingflow_mcp/cli/commands/task.py +74 -22
- package/src/qingflow_mcp/cli/commands/workspace.py +22 -0
- package/src/qingflow_mcp/cli/formatters.py +287 -48
- package/src/qingflow_mcp/cli/main.py +6 -1
- package/src/qingflow_mcp/cli/qingflow_login.py +116 -0
- package/src/qingflow_mcp/config.py +1 -1
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/id_utils.py +49 -0
- package/src/qingflow_mcp/public_surface.py +11 -1
- package/src/qingflow_mcp/response_trim.py +380 -9
- package/src/qingflow_mcp/server.py +4 -0
- package/src/qingflow_mcp/server_app_builder.py +11 -1
- package/src/qingflow_mcp/server_app_user.py +24 -0
- package/src/qingflow_mcp/session_store.py +69 -15
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- package/src/qingflow_mcp/tools/ai_builder_tools.py +48 -18
- package/src/qingflow_mcp/tools/app_tools.py +1 -0
- package/src/qingflow_mcp/tools/auth_tools.py +217 -9
- package/src/qingflow_mcp/tools/base.py +6 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +2 -2
- package/src/qingflow_mcp/tools/import_tools.py +36 -2
- package/src/qingflow_mcp/tools/record_tools.py +410 -156
- package/src/qingflow_mcp/tools/resource_read_tools.py +114 -32
- package/src/qingflow_mcp/tools/task_context_tools.py +899 -141
- package/src/qingflow_mcp/tools/workspace_tools.py +141 -0
|
@@ -7,7 +7,6 @@ from ..errors import QingflowApiError, raise_tool_error
|
|
|
7
7
|
from ..json_types import JSONObject
|
|
8
8
|
from ..list_type_labels import SYSTEM_VIEW_DEFINITIONS
|
|
9
9
|
from .app_tools import _analysis_supported_for_view_type
|
|
10
|
-
from .app_tools import AppTools
|
|
11
10
|
from .base import ToolBase, tool_cn_name
|
|
12
11
|
from .qingbi_report_tools import QingbiReportTools
|
|
13
12
|
|
|
@@ -25,7 +24,6 @@ class ResourceReadTools(ToolBase):
|
|
|
25
24
|
def __init__(self, sessions, backend) -> None:
|
|
26
25
|
"""执行内部辅助逻辑。"""
|
|
27
26
|
super().__init__(sessions, backend)
|
|
28
|
-
self.apps = AppTools(sessions, backend)
|
|
29
27
|
self.charts = QingbiReportTools(sessions, backend)
|
|
30
28
|
|
|
31
29
|
@tool_cn_name("资源读取-门户列表")
|
|
@@ -153,9 +151,11 @@ class ResourceReadTools(ToolBase):
|
|
|
153
151
|
or str(config.get("viewgraphType") or config.get("viewType") or "").strip()
|
|
154
152
|
)
|
|
155
153
|
resolved_app_key = str(base_info.get("appKey") or config.get("appKey") or "").strip() or None
|
|
154
|
+
if not resolved_app_key:
|
|
155
|
+
resolved_app_key = self._resolve_app_key_from_view_form(context=context, view_key=view_key)
|
|
156
156
|
if not resolved_app_key:
|
|
157
157
|
resolved_app_key = self._resolve_app_key_from_form_id(
|
|
158
|
-
|
|
158
|
+
context=context,
|
|
159
159
|
form_id=_coerce_positive_int(base_info.get("formId") or config.get("formId")),
|
|
160
160
|
)
|
|
161
161
|
if not resolved_app_key:
|
|
@@ -198,25 +198,56 @@ class ResourceReadTools(ToolBase):
|
|
|
198
198
|
|
|
199
199
|
def runner(session_profile, _context):
|
|
200
200
|
base = self.charts.qingbi_report_get_base(profile=profile, chart_id=chart_id).get("result") or {}
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
warnings: list[JSONObject] = []
|
|
202
|
+
verification = {
|
|
203
|
+
"chart_exists": True,
|
|
204
|
+
"chart_data_loaded": False,
|
|
205
|
+
"chart_config_loaded": False,
|
|
206
|
+
}
|
|
207
|
+
data: Any = None
|
|
208
|
+
data_config: dict[str, Any] = {}
|
|
209
|
+
try:
|
|
210
|
+
data = self.charts.qingbi_report_get_data(profile=profile, chart_id=chart_id, payload={}).get("result") or {}
|
|
211
|
+
verification["chart_data_loaded"] = True
|
|
212
|
+
if isinstance(data, dict) and isinstance(data.get("config"), dict):
|
|
213
|
+
data_config = deepcopy(data.get("config"))
|
|
214
|
+
verification["chart_config_loaded"] = True
|
|
215
|
+
except (QingflowApiError, RuntimeError) as error:
|
|
216
|
+
api_error = error if isinstance(error, QingflowApiError) else None
|
|
217
|
+
warnings.append(
|
|
218
|
+
{
|
|
219
|
+
"code": "CHART_DATA_UNAVAILABLE",
|
|
220
|
+
"message": "chart_get could not load chart data; returning chart metadata and config when available.",
|
|
221
|
+
"backend_code": api_error.backend_code if api_error is not None else None,
|
|
222
|
+
"http_status": api_error.http_status if api_error is not None else None,
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
if not data_config:
|
|
226
|
+
try:
|
|
227
|
+
config_result = self.charts.qingbi_report_get_config(profile=profile, chart_id=chart_id).get("result") or {}
|
|
228
|
+
except (QingflowApiError, RuntimeError):
|
|
229
|
+
config_result = {}
|
|
230
|
+
if isinstance(config_result, dict) and config_result:
|
|
231
|
+
data_config = deepcopy(config_result)
|
|
232
|
+
verification["chart_config_loaded"] = True
|
|
233
|
+
data_payload: dict[str, Any] = {
|
|
234
|
+
"chart_id": chart_id,
|
|
235
|
+
"chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
|
|
236
|
+
"chart_type": str(base.get("chartType") or data_config.get("chartType") or "").strip() or None,
|
|
237
|
+
"data_source_type": str(base.get("dataSourceType") or "").strip() or None,
|
|
238
|
+
"data_source_id": str(base.get("dataSourceId") or "").strip() or None,
|
|
239
|
+
}
|
|
240
|
+
if verification["chart_data_loaded"]:
|
|
241
|
+
data_payload["data"] = deepcopy(data) if isinstance(data, dict) else {"value": data}
|
|
242
|
+
if data_config and not verification["chart_data_loaded"]:
|
|
243
|
+
data_payload["config"] = deepcopy(data_config)
|
|
203
244
|
return {
|
|
204
245
|
"profile": profile,
|
|
205
246
|
"ws_id": session_profile.selected_ws_id,
|
|
206
247
|
"ok": True,
|
|
207
|
-
"warnings":
|
|
208
|
-
"verification":
|
|
209
|
-
|
|
210
|
-
"chart_data_loaded": True,
|
|
211
|
-
},
|
|
212
|
-
"data": {
|
|
213
|
-
"chart_id": chart_id,
|
|
214
|
-
"chart_name": str(base.get("chartName") or base.get("name") or chart_id).strip() or chart_id,
|
|
215
|
-
"chart_type": str(base.get("chartType") or data_config.get("chartType") or "").strip() or None,
|
|
216
|
-
"data_source_type": str(base.get("dataSourceType") or "").strip() or None,
|
|
217
|
-
"data_source_id": str(base.get("dataSourceId") or "").strip() or None,
|
|
218
|
-
"data": deepcopy(data) if isinstance(data, dict) else {"value": data},
|
|
219
|
-
},
|
|
248
|
+
"warnings": warnings,
|
|
249
|
+
"verification": verification,
|
|
250
|
+
"data": data_payload,
|
|
220
251
|
}
|
|
221
252
|
|
|
222
253
|
return self._run(profile, runner)
|
|
@@ -236,25 +267,54 @@ class ResourceReadTools(ToolBase):
|
|
|
236
267
|
if not chart_id:
|
|
237
268
|
raise_tool_error(QingflowApiError.config_error("chart_id is required"))
|
|
238
269
|
|
|
239
|
-
def
|
|
270
|
+
def _resolve_app_key_from_view_form(self, *, context: Any, view_key: str) -> str | None:
|
|
271
|
+
try:
|
|
272
|
+
form_payload = self.backend.request("GET", context, f"/view/{view_key}/form")
|
|
273
|
+
except QingflowApiError:
|
|
274
|
+
return None
|
|
275
|
+
if not isinstance(form_payload, dict):
|
|
276
|
+
return None
|
|
277
|
+
app_key = str(form_payload.get("appKey") or "").strip()
|
|
278
|
+
return app_key or None
|
|
279
|
+
|
|
280
|
+
def _resolve_app_key_from_form_id(self, *, context: Any, form_id: int | None) -> str | None:
|
|
240
281
|
"""执行内部辅助逻辑。"""
|
|
241
282
|
if form_id is None:
|
|
242
283
|
return None
|
|
243
284
|
try:
|
|
244
|
-
payload = self.
|
|
285
|
+
payload = self.backend.request("GET", context, "/tag/apps")
|
|
245
286
|
except QingflowApiError:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
payload = None
|
|
288
|
+
app_key = _find_visible_app_key_by_form_id(payload, form_id=form_id)
|
|
289
|
+
if app_key:
|
|
290
|
+
return app_key
|
|
291
|
+
page_num = 1
|
|
292
|
+
page_size = 200
|
|
293
|
+
while page_num <= 20:
|
|
294
|
+
try:
|
|
295
|
+
page = self.backend.request(
|
|
296
|
+
"GET",
|
|
297
|
+
context,
|
|
298
|
+
"/app/item",
|
|
299
|
+
params={"pageNum": page_num, "pageSize": page_size},
|
|
300
|
+
)
|
|
301
|
+
except QingflowApiError:
|
|
302
|
+
return None
|
|
303
|
+
items = page.get("list") if isinstance(page, dict) else []
|
|
304
|
+
if not isinstance(items, list) or not items:
|
|
305
|
+
return None
|
|
306
|
+
for item in items:
|
|
307
|
+
if not isinstance(item, dict):
|
|
308
|
+
continue
|
|
309
|
+
if _coerce_positive_int(item.get("formId") or item.get("form_id")) != form_id:
|
|
310
|
+
continue
|
|
311
|
+
app_key = str(item.get("appKey") or item.get("app_key") or "").strip()
|
|
312
|
+
if app_key:
|
|
313
|
+
return app_key
|
|
314
|
+
total = _coerce_positive_int(page.get("total")) if isinstance(page, dict) else None
|
|
315
|
+
if total is not None and page_num * page_size >= total:
|
|
316
|
+
break
|
|
317
|
+
page_num += 1
|
|
258
318
|
return None
|
|
259
319
|
|
|
260
320
|
|
|
@@ -419,3 +479,25 @@ def _coerce_positive_int(value: Any) -> int | None:
|
|
|
419
479
|
except (TypeError, ValueError):
|
|
420
480
|
return None
|
|
421
481
|
return number if number > 0 else None
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _find_visible_app_key_by_form_id(payload: Any, *, form_id: int) -> str | None:
|
|
485
|
+
if isinstance(payload, list):
|
|
486
|
+
for item in payload:
|
|
487
|
+
resolved = _find_visible_app_key_by_form_id(item, form_id=form_id)
|
|
488
|
+
if resolved:
|
|
489
|
+
return resolved
|
|
490
|
+
return None
|
|
491
|
+
if not isinstance(payload, dict):
|
|
492
|
+
return None
|
|
493
|
+
candidate_form_id = _coerce_positive_int(payload.get("formId") or payload.get("form_id"))
|
|
494
|
+
if candidate_form_id == form_id:
|
|
495
|
+
app_key = str(payload.get("appKey") or payload.get("app_key") or "").strip()
|
|
496
|
+
if app_key:
|
|
497
|
+
return app_key
|
|
498
|
+
for value in payload.values():
|
|
499
|
+
if isinstance(value, (list, dict)):
|
|
500
|
+
resolved = _find_visible_app_key_by_form_id(value, form_id=form_id)
|
|
501
|
+
if resolved:
|
|
502
|
+
return resolved
|
|
503
|
+
return None
|