@qingflow-tech/qingflow-app-user-mcp 1.0.0
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 +37 -0
- package/docs/local-agent-install.md +332 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow-app-user-mcp.mjs +7 -0
- package/npm/lib/runtime.mjs +339 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow-app-user-mcp +15 -0
- package/skills/qingflow-app-user/SKILL.md +79 -0
- package/skills/qingflow-app-user/agents/openai.yaml +4 -0
- package/skills/qingflow-app-user/references/data-gotchas.md +29 -0
- package/skills/qingflow-app-user/references/environments.md +63 -0
- package/skills/qingflow-app-user/references/record-patterns.md +48 -0
- package/skills/qingflow-app-user/references/workflow-usage.md +26 -0
- package/skills/qingflow-record-analysis/SKILL.md +158 -0
- package/skills/qingflow-record-analysis/agents/openai.yaml +4 -0
- package/skills/qingflow-record-analysis/references/analysis-gotchas.md +145 -0
- package/skills/qingflow-record-analysis/references/analysis-patterns.md +125 -0
- package/skills/qingflow-record-analysis/references/confidence-reporting.md +92 -0
- package/skills/qingflow-record-analysis/references/dsl-templates.md +93 -0
- package/skills/qingflow-record-delete/SKILL.md +29 -0
- package/skills/qingflow-record-import/SKILL.md +31 -0
- package/skills/qingflow-record-insert/SKILL.md +58 -0
- package/skills/qingflow-record-update/SKILL.md +42 -0
- package/skills/qingflow-task-ops/SKILL.md +123 -0
- package/skills/qingflow-task-ops/agents/openai.yaml +4 -0
- package/skills/qingflow-task-ops/references/environments.md +44 -0
- package/skills/qingflow-task-ops/references/workflow-usage.md +27 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +649 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +1836 -0
- package/src/qingflow_mcp/builder_facade/service.py +15044 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +44 -0
- package/src/qingflow_mcp/cli/commands/builder.py +538 -0
- package/src/qingflow_mcp/cli/commands/chart.py +18 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/portal.py +25 -0
- package/src/qingflow_mcp/cli/commands/record.py +331 -0
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +89 -0
- package/src/qingflow_mcp/cli/commands/view.py +18 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +25 -0
- package/src/qingflow_mcp/cli/context.py +60 -0
- package/src/qingflow_mcp/cli/formatters.py +334 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +178 -0
- package/src/qingflow_mcp/config.py +513 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/public_surface.py +233 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +470 -0
- package/src/qingflow_mcp/server.py +212 -0
- package/src/qingflow_mcp/server_app_builder.py +533 -0
- package/src/qingflow_mcp/server_app_user.py +362 -0
- package/src/qingflow_mcp/session_store.py +302 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2398 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +855 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3419 -0
- package/src/qingflow_mcp/tools/app_tools.py +925 -0
- package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
- package/src/qingflow_mcp/tools/auth_tools.py +875 -0
- package/src/qingflow_mcp/tools/base.py +388 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
- package/src/qingflow_mcp/tools/directory_tools.py +675 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
- package/src/qingflow_mcp/tools/file_tools.py +409 -0
- package/src/qingflow_mcp/tools/import_tools.py +2189 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
- package/src/qingflow_mcp/tools/package_tools.py +326 -0
- package/src/qingflow_mcp/tools/portal_tools.py +158 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
- package/src/qingflow_mcp/tools/record_tools.py +14037 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +421 -0
- package/src/qingflow_mcp/tools/role_tools.py +112 -0
- package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +2228 -0
- package/src/qingflow_mcp/tools/task_tools.py +890 -0
- package/src/qingflow_mcp/tools/view_tools.py +335 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +125 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
from contextvars import ContextVar
|
|
5
|
+
from decimal import Decimal, InvalidOperation
|
|
6
|
+
from typing import Any, Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
from ..backend_client import BackendRequestContext, BackendClient, BackendResponse
|
|
9
|
+
from ..config import (
|
|
10
|
+
get_credit_balance_base_url,
|
|
11
|
+
get_credit_balance_path,
|
|
12
|
+
get_credit_balance_token_key,
|
|
13
|
+
get_credit_balance_token_value,
|
|
14
|
+
get_credit_balance_ws_id_header_key,
|
|
15
|
+
get_credit_meter_enabled,
|
|
16
|
+
get_credit_usage_amount,
|
|
17
|
+
get_credit_usage_base_url,
|
|
18
|
+
get_credit_usage_path,
|
|
19
|
+
get_credit_usage_token_key,
|
|
20
|
+
get_credit_usage_token_value,
|
|
21
|
+
get_credit_usage_ws_id_header_key,
|
|
22
|
+
)
|
|
23
|
+
from ..errors import QingflowApiError, raise_tool_error
|
|
24
|
+
from ..json_types import JSONObject
|
|
25
|
+
from ..session_store import BackendSession, SessionProfile, SessionStore
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
T = TypeVar("T")
|
|
29
|
+
_RUN_DEPTH: ContextVar[int] = ContextVar("qingflow_mcp_tool_run_depth", default=0)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def tool_cn_name(name: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
33
|
+
"""为 tools 方法声明中文展示名(用于计费 extraInfo 记录)。"""
|
|
34
|
+
normalized = str(name).strip()
|
|
35
|
+
|
|
36
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
37
|
+
setattr(func, "__tool_cn_name__", normalized)
|
|
38
|
+
return func
|
|
39
|
+
|
|
40
|
+
return decorator
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ToolBase:
|
|
44
|
+
def __init__(self, sessions: SessionStore, backend: BackendClient) -> None:
|
|
45
|
+
self.sessions = sessions
|
|
46
|
+
self.backend = backend
|
|
47
|
+
|
|
48
|
+
def _require_context(self, profile: str, *, require_workspace: bool) -> tuple[SessionProfile, BackendSession, BackendRequestContext]:
|
|
49
|
+
session_profile = self.sessions.get_profile(profile)
|
|
50
|
+
if session_profile is None:
|
|
51
|
+
raise QingflowApiError.auth_required(profile)
|
|
52
|
+
backend_session = self.sessions.get_backend_session(profile)
|
|
53
|
+
if backend_session is None:
|
|
54
|
+
raise QingflowApiError.auth_required(profile)
|
|
55
|
+
if require_workspace and session_profile.selected_ws_id is None:
|
|
56
|
+
raise QingflowApiError.workspace_not_selected(profile)
|
|
57
|
+
context = BackendRequestContext(
|
|
58
|
+
base_url=backend_session.base_url,
|
|
59
|
+
token=backend_session.token,
|
|
60
|
+
ws_id=session_profile.selected_ws_id if require_workspace else None,
|
|
61
|
+
qf_version=backend_session.qf_version,
|
|
62
|
+
qf_version_source=backend_session.qf_version_source,
|
|
63
|
+
)
|
|
64
|
+
return session_profile, backend_session, context
|
|
65
|
+
|
|
66
|
+
def _run(self, profile: str, func: Callable[[SessionProfile, BackendRequestContext], T], *, require_workspace: bool = True) -> T:
|
|
67
|
+
depth = _RUN_DEPTH.get()
|
|
68
|
+
depth_token = _RUN_DEPTH.set(depth + 1)
|
|
69
|
+
is_top_level_call = depth == 0
|
|
70
|
+
session_profile: SessionProfile | None = None
|
|
71
|
+
backend_session: BackendSession | None = None
|
|
72
|
+
tool_name = self._resolve_calling_tool_name()
|
|
73
|
+
try:
|
|
74
|
+
session_profile, backend_session, context = self._require_context(profile, require_workspace=require_workspace)
|
|
75
|
+
if is_top_level_call:
|
|
76
|
+
self._charge_tool_call(
|
|
77
|
+
profile=profile,
|
|
78
|
+
tool_name=tool_name,
|
|
79
|
+
session_profile=session_profile,
|
|
80
|
+
context=context,
|
|
81
|
+
)
|
|
82
|
+
return func(session_profile, context)
|
|
83
|
+
except QingflowApiError as error:
|
|
84
|
+
if (
|
|
85
|
+
error.looks_like_invalid_token()
|
|
86
|
+
and session_profile is not None
|
|
87
|
+
and backend_session is not None
|
|
88
|
+
and self._refresh_session_from_credential(
|
|
89
|
+
profile,
|
|
90
|
+
session_profile=session_profile,
|
|
91
|
+
backend_session=backend_session,
|
|
92
|
+
)
|
|
93
|
+
):
|
|
94
|
+
try:
|
|
95
|
+
refreshed_profile, _, refreshed_context = self._require_context(profile, require_workspace=require_workspace)
|
|
96
|
+
return func(refreshed_profile, refreshed_context)
|
|
97
|
+
except QingflowApiError as refreshed_error:
|
|
98
|
+
self._handle_error(profile, refreshed_error)
|
|
99
|
+
self._handle_error(profile, error)
|
|
100
|
+
finally:
|
|
101
|
+
_RUN_DEPTH.reset(depth_token)
|
|
102
|
+
raise AssertionError("unreachable")
|
|
103
|
+
|
|
104
|
+
def _handle_error(self, profile: str, error: QingflowApiError) -> None:
|
|
105
|
+
if error.looks_like_invalid_token():
|
|
106
|
+
self.sessions.invalidate(profile)
|
|
107
|
+
error = QingflowApiError(
|
|
108
|
+
category="auth",
|
|
109
|
+
message=f"Qingflow session for profile '{profile}' has expired. Run auth_use_credential again.",
|
|
110
|
+
backend_code=error.backend_code,
|
|
111
|
+
request_id=error.request_id,
|
|
112
|
+
http_status=error.http_status,
|
|
113
|
+
)
|
|
114
|
+
raise_tool_error(error)
|
|
115
|
+
|
|
116
|
+
def _refresh_session_from_credential(
|
|
117
|
+
self,
|
|
118
|
+
profile: str,
|
|
119
|
+
*,
|
|
120
|
+
session_profile: SessionProfile,
|
|
121
|
+
backend_session: BackendSession,
|
|
122
|
+
) -> bool:
|
|
123
|
+
credential = (backend_session.credential or "").strip()
|
|
124
|
+
if not credential:
|
|
125
|
+
return False
|
|
126
|
+
try:
|
|
127
|
+
response = self.backend.public_request_with_meta(
|
|
128
|
+
"POST",
|
|
129
|
+
session_profile.base_url,
|
|
130
|
+
"/mcp/auth/context",
|
|
131
|
+
json_body={"credential": credential},
|
|
132
|
+
qf_version=backend_session.qf_version,
|
|
133
|
+
)
|
|
134
|
+
payload = self._unwrap_context_payload(response.data)
|
|
135
|
+
token = self._normalize_text(payload.get("token"))
|
|
136
|
+
ws_id = self._coerce_positive_int(payload.get("wsId"))
|
|
137
|
+
uid = self._coerce_positive_int(payload.get("uid")) or session_profile.uid
|
|
138
|
+
if not token or ws_id is None:
|
|
139
|
+
return False
|
|
140
|
+
backend_qf_version = self._normalize_text(payload.get("qfVersion")) or response.qf_response_version
|
|
141
|
+
qf_version = backend_qf_version if backend_qf_version is not None else backend_session.qf_version
|
|
142
|
+
qf_version_source = (
|
|
143
|
+
"backend_response"
|
|
144
|
+
if backend_qf_version is not None
|
|
145
|
+
else backend_session.qf_version_source
|
|
146
|
+
)
|
|
147
|
+
base_url = self._normalize_text(payload.get("baseUrl")) or session_profile.base_url
|
|
148
|
+
ws_name = self._normalize_text(payload.get("wsName")) or session_profile.selected_ws_name
|
|
149
|
+
self.sessions.save_session(
|
|
150
|
+
profile=profile,
|
|
151
|
+
base_url=base_url,
|
|
152
|
+
qf_version=qf_version,
|
|
153
|
+
qf_version_source=qf_version_source,
|
|
154
|
+
token=token,
|
|
155
|
+
login_token=None,
|
|
156
|
+
credential=credential,
|
|
157
|
+
uid=uid,
|
|
158
|
+
email=self._normalize_text(payload.get("email")) or session_profile.email,
|
|
159
|
+
nick_name=(
|
|
160
|
+
self._normalize_text(payload.get("nickName"))
|
|
161
|
+
or self._normalize_text(payload.get("displayName"))
|
|
162
|
+
or self._normalize_text(payload.get("name"))
|
|
163
|
+
or session_profile.nick_name
|
|
164
|
+
),
|
|
165
|
+
persist=session_profile.persisted,
|
|
166
|
+
)
|
|
167
|
+
self.sessions.select_workspace(profile, ws_id=ws_id, ws_name=ws_name)
|
|
168
|
+
return True
|
|
169
|
+
except QingflowApiError:
|
|
170
|
+
return False
|
|
171
|
+
|
|
172
|
+
def _unwrap_context_payload(self, payload: Any) -> dict[str, Any]:
|
|
173
|
+
if not isinstance(payload, dict):
|
|
174
|
+
return {}
|
|
175
|
+
nested = payload.get("data")
|
|
176
|
+
if isinstance(nested, dict):
|
|
177
|
+
return nested
|
|
178
|
+
nested = payload.get("result")
|
|
179
|
+
if isinstance(nested, dict):
|
|
180
|
+
return nested
|
|
181
|
+
return payload
|
|
182
|
+
|
|
183
|
+
def _normalize_text(self, value: Any) -> str | None:
|
|
184
|
+
if value is None:
|
|
185
|
+
return None
|
|
186
|
+
text = str(value).strip()
|
|
187
|
+
return text or None
|
|
188
|
+
|
|
189
|
+
def _coerce_positive_int(self, value: Any) -> int | None:
|
|
190
|
+
if isinstance(value, bool) or value is None:
|
|
191
|
+
return None
|
|
192
|
+
try:
|
|
193
|
+
parsed = int(value)
|
|
194
|
+
except (TypeError, ValueError):
|
|
195
|
+
return None
|
|
196
|
+
return parsed if parsed > 0 else None
|
|
197
|
+
|
|
198
|
+
def _resolve_calling_tool_name(self) -> str:
|
|
199
|
+
frame = inspect.currentframe()
|
|
200
|
+
if frame is None:
|
|
201
|
+
return "unknown_tool"
|
|
202
|
+
caller = frame.f_back.f_back if frame.f_back is not None else None
|
|
203
|
+
try:
|
|
204
|
+
if caller is None:
|
|
205
|
+
return "unknown_tool"
|
|
206
|
+
name = caller.f_code.co_name
|
|
207
|
+
if not name:
|
|
208
|
+
return "unknown_tool"
|
|
209
|
+
method = getattr(self, name, None)
|
|
210
|
+
cn_name = getattr(method, "__tool_cn_name__", None)
|
|
211
|
+
if isinstance(cn_name, str) and cn_name.strip():
|
|
212
|
+
return cn_name.strip()
|
|
213
|
+
return name
|
|
214
|
+
finally:
|
|
215
|
+
del frame
|
|
216
|
+
|
|
217
|
+
def _charge_tool_call(
|
|
218
|
+
self,
|
|
219
|
+
*,
|
|
220
|
+
profile: str,
|
|
221
|
+
tool_name: str,
|
|
222
|
+
session_profile: SessionProfile,
|
|
223
|
+
context: BackendRequestContext,
|
|
224
|
+
) -> None:
|
|
225
|
+
if not get_credit_meter_enabled():
|
|
226
|
+
return
|
|
227
|
+
if not self._credit_meter_is_ready():
|
|
228
|
+
return
|
|
229
|
+
if context.ws_id is None:
|
|
230
|
+
raise_tool_error(QingflowApiError(category="payment", message="credit meter requires wsId in current session context"))
|
|
231
|
+
|
|
232
|
+
usage_amount = self._read_usage_amount()
|
|
233
|
+
available_balance = self._fetch_credit_balance(context.ws_id)
|
|
234
|
+
if available_balance < usage_amount:
|
|
235
|
+
raise_tool_error(
|
|
236
|
+
QingflowApiError(
|
|
237
|
+
category="payment",
|
|
238
|
+
message=f"insufficient credit balance: available={available_balance}, required={usage_amount}",
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
self._record_credit_usage(
|
|
242
|
+
tool_name=tool_name,
|
|
243
|
+
ws_id=context.ws_id,
|
|
244
|
+
uid=session_profile.uid,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def _credit_meter_is_ready(self) -> bool:
|
|
248
|
+
return bool(
|
|
249
|
+
get_credit_balance_base_url()
|
|
250
|
+
and get_credit_balance_token_value()
|
|
251
|
+
and get_credit_usage_base_url()
|
|
252
|
+
and get_credit_usage_token_value()
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
def _read_usage_amount(self) -> Decimal:
|
|
256
|
+
raw_amount = get_credit_usage_amount()
|
|
257
|
+
try:
|
|
258
|
+
amount = Decimal(str(raw_amount).strip())
|
|
259
|
+
except (InvalidOperation, ValueError):
|
|
260
|
+
raise_tool_error(
|
|
261
|
+
QingflowApiError.config_error(
|
|
262
|
+
"QINGFLOW_MCP_CREDIT_APAAS_AMOUNT must be a positive number"
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
if amount <= 0:
|
|
266
|
+
raise_tool_error(
|
|
267
|
+
QingflowApiError.config_error(
|
|
268
|
+
"QINGFLOW_MCP_CREDIT_APAAS_AMOUNT must be a positive number"
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
return amount
|
|
272
|
+
|
|
273
|
+
def _fetch_credit_balance(self, ws_id: int) -> Decimal:
|
|
274
|
+
balance_base_url = get_credit_balance_base_url()
|
|
275
|
+
balance_token_key = get_credit_balance_token_key()
|
|
276
|
+
balance_token_value = get_credit_balance_token_value()
|
|
277
|
+
ws_id_header_key = get_credit_balance_ws_id_header_key()
|
|
278
|
+
if not balance_base_url:
|
|
279
|
+
raise_tool_error(
|
|
280
|
+
QingflowApiError.config_error(
|
|
281
|
+
"QINGFLOW_MCP_CREDIT_WINGS_BASE_URL is required when credit meter is enabled"
|
|
282
|
+
)
|
|
283
|
+
)
|
|
284
|
+
if not balance_token_value:
|
|
285
|
+
raise_tool_error(
|
|
286
|
+
QingflowApiError.config_error(
|
|
287
|
+
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_VALUE is required when credit meter is enabled"
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
response = self.backend.public_request_with_headers(
|
|
291
|
+
"GET",
|
|
292
|
+
balance_base_url,
|
|
293
|
+
get_credit_balance_path(),
|
|
294
|
+
headers={
|
|
295
|
+
balance_token_key: balance_token_value,
|
|
296
|
+
ws_id_header_key: str(ws_id),
|
|
297
|
+
},
|
|
298
|
+
)
|
|
299
|
+
payload: Any = response.data if isinstance(response, BackendResponse) else response
|
|
300
|
+
if isinstance(payload, dict):
|
|
301
|
+
daily = self._coerce_decimal(payload.get("dailyBalance"))
|
|
302
|
+
monthly = self._coerce_decimal(payload.get("monthlyBalance"))
|
|
303
|
+
permanent = self._coerce_decimal(payload.get("balance"))
|
|
304
|
+
if daily is not None or monthly is not None or permanent is not None:
|
|
305
|
+
return (daily or Decimal("0")) + (monthly or Decimal("0")) + (permanent or Decimal("0"))
|
|
306
|
+
raise_tool_error(
|
|
307
|
+
QingflowApiError(
|
|
308
|
+
category="payment",
|
|
309
|
+
message="credit balance response is invalid: expected dailyBalance/monthlyBalance/balance",
|
|
310
|
+
)
|
|
311
|
+
)
|
|
312
|
+
raise AssertionError("unreachable")
|
|
313
|
+
|
|
314
|
+
def _record_credit_usage(
|
|
315
|
+
self,
|
|
316
|
+
*,
|
|
317
|
+
tool_name: str,
|
|
318
|
+
ws_id: int,
|
|
319
|
+
uid: int,
|
|
320
|
+
) -> None:
|
|
321
|
+
usage_base_url = get_credit_usage_base_url()
|
|
322
|
+
usage_token_key = get_credit_usage_token_key()
|
|
323
|
+
usage_token_value = get_credit_usage_token_value()
|
|
324
|
+
ws_id_header_key = get_credit_usage_ws_id_header_key()
|
|
325
|
+
if not usage_base_url:
|
|
326
|
+
raise_tool_error(
|
|
327
|
+
QingflowApiError.config_error(
|
|
328
|
+
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL is required when credit meter is enabled"
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
if not usage_token_value:
|
|
332
|
+
raise_tool_error(
|
|
333
|
+
QingflowApiError.config_error(
|
|
334
|
+
"QINGFLOW_MCP_CREDIT_APAAS_TOKEN_VALUE is required when credit meter is enabled"
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
self.backend.public_request_with_headers(
|
|
338
|
+
"POST",
|
|
339
|
+
usage_base_url,
|
|
340
|
+
get_credit_usage_path(),
|
|
341
|
+
headers={
|
|
342
|
+
usage_token_key: usage_token_value,
|
|
343
|
+
ws_id_header_key: str(ws_id),
|
|
344
|
+
},
|
|
345
|
+
json_body={
|
|
346
|
+
"wsId": ws_id,
|
|
347
|
+
"uid": uid,
|
|
348
|
+
"creditUsage": "1",
|
|
349
|
+
"businessType": "WORKSPACE",
|
|
350
|
+
"scene": "MCP",
|
|
351
|
+
"aiBiz": "mcp",
|
|
352
|
+
"extraInfo": tool_name,
|
|
353
|
+
},
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def _coerce_decimal(self, value: Any) -> Decimal | None:
|
|
357
|
+
if value is None or isinstance(value, bool):
|
|
358
|
+
return None
|
|
359
|
+
try:
|
|
360
|
+
return Decimal(str(value))
|
|
361
|
+
except (InvalidOperation, ValueError):
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
def _require_dict(self, payload: JSONObject | None, field_name: str = "payload") -> JSONObject:
|
|
365
|
+
if not isinstance(payload, dict) or not payload:
|
|
366
|
+
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be a non-empty object"))
|
|
367
|
+
return payload
|
|
368
|
+
|
|
369
|
+
def _high_risk_tool_description(self, *, operation: str, target: str) -> str:
|
|
370
|
+
return (
|
|
371
|
+
f"High-risk {operation} operation for {target}. Read the current state first, "
|
|
372
|
+
"confirm the exact target IDs and intended diff with a human, and avoid running "
|
|
373
|
+
"against production without explicit approval."
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
def _attach_human_review_notice(self, response: JSONObject, *, operation: str, target: str) -> JSONObject:
|
|
377
|
+
payload = dict(response)
|
|
378
|
+
payload["requires_human_review"] = True
|
|
379
|
+
payload["risk_notice"] = {
|
|
380
|
+
"operation": operation,
|
|
381
|
+
"target": target,
|
|
382
|
+
"severity": "high",
|
|
383
|
+
"guidance": (
|
|
384
|
+
"Read the current state first, confirm the exact target IDs and intended diff with a human, "
|
|
385
|
+
"and require explicit approval before running against production."
|
|
386
|
+
),
|
|
387
|
+
}
|
|
388
|
+
return payload
|