@qingflow-tech/qingflow-app-builder-mcp 1.0.0 → 1.0.1
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 +8 -37
- package/package.json +1 -1
- package/src/qingflow_mcp/config.py +1 -115
- package/src/qingflow_mcp/session_store.py +20 -7
- package/src/qingflow_mcp/tools/auth_tools.py +0 -1
- package/src/qingflow_mcp/tools/base.py +17 -128
- package/src/qingflow_mcp/tools/task_tools.py +0 -1
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-builder-mcp@1.0.1
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-builder-mcp@1.0.1 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
|
@@ -152,14 +152,8 @@ qingflow-app-builder-mcp
|
|
|
152
152
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
153
153
|
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
|
|
154
154
|
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
155
|
-
"QINGFLOW_MCP_CREDIT_WINGS_BASE_URL": "https://ultron.internal.example.com",
|
|
156
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_KEY": "wingsTokenKey",
|
|
157
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_VALUE": "replace-prod-balance-token",
|
|
158
155
|
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
159
|
-
"
|
|
160
|
-
"QINGFLOW_MCP_CREDIT_APAAS_TOKEN_VALUE": "729ed3cc-8eea-11ec-b585-52540009137b",
|
|
161
|
-
"QINGFLOW_MCP_CREDIT_WS_ID_HEADER_KEY": "wsId",
|
|
162
|
-
"QINGFLOW_MCP_CREDIT_APAAS_AMOUNT": "1"
|
|
156
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
163
157
|
}
|
|
164
158
|
}
|
|
165
159
|
}
|
|
@@ -178,14 +172,8 @@ qingflow-app-builder-mcp
|
|
|
178
172
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
179
173
|
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
|
|
180
174
|
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
181
|
-
"QINGFLOW_MCP_CREDIT_WINGS_BASE_URL": "https://ultron.internal.example.com",
|
|
182
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_KEY": "wingsTokenKey",
|
|
183
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_VALUE": "replace-prod-balance-token",
|
|
184
175
|
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
185
|
-
"
|
|
186
|
-
"QINGFLOW_MCP_CREDIT_APAAS_TOKEN_VALUE": "729ed3cc-8eea-11ec-b585-52540009137b",
|
|
187
|
-
"QINGFLOW_MCP_CREDIT_WS_ID_HEADER_KEY": "wsId",
|
|
188
|
-
"QINGFLOW_MCP_CREDIT_APAAS_AMOUNT": "1"
|
|
176
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
189
177
|
}
|
|
190
178
|
}
|
|
191
179
|
}
|
|
@@ -204,14 +192,8 @@ qingflow-app-builder-mcp
|
|
|
204
192
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
205
193
|
"QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
|
|
206
194
|
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
207
|
-
"QINGFLOW_MCP_CREDIT_WINGS_BASE_URL": "https://ultron.internal.example.com",
|
|
208
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_KEY": "wingsTokenKey",
|
|
209
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_VALUE": "replace-prod-balance-token",
|
|
210
195
|
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
211
|
-
"
|
|
212
|
-
"QINGFLOW_MCP_CREDIT_APAAS_TOKEN_VALUE": "729ed3cc-8eea-11ec-b585-52540009137b",
|
|
213
|
-
"QINGFLOW_MCP_CREDIT_WS_ID_HEADER_KEY": "wsId",
|
|
214
|
-
"QINGFLOW_MCP_CREDIT_APAAS_AMOUNT": "1"
|
|
196
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
215
197
|
}
|
|
216
198
|
}
|
|
217
199
|
}
|
|
@@ -234,14 +216,8 @@ qingflow-app-builder-mcp
|
|
|
234
216
|
"env": {
|
|
235
217
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
236
218
|
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
237
|
-
"QINGFLOW_MCP_CREDIT_WINGS_BASE_URL": "https://ultron.internal.example.com",
|
|
238
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_KEY": "wingsTokenKey",
|
|
239
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_VALUE": "replace-prod-balance-token",
|
|
240
219
|
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
241
|
-
"
|
|
242
|
-
"QINGFLOW_MCP_CREDIT_APAAS_TOKEN_VALUE": "729ed3cc-8eea-11ec-b585-52540009137b",
|
|
243
|
-
"QINGFLOW_MCP_CREDIT_WS_ID_HEADER_KEY": "wsId",
|
|
244
|
-
"QINGFLOW_MCP_CREDIT_APAAS_AMOUNT": "1"
|
|
220
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
245
221
|
}
|
|
246
222
|
},
|
|
247
223
|
"qingflow-builder": {
|
|
@@ -253,14 +229,8 @@ qingflow-app-builder-mcp
|
|
|
253
229
|
"env": {
|
|
254
230
|
"QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
|
|
255
231
|
"QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
|
|
256
|
-
"QINGFLOW_MCP_CREDIT_WINGS_BASE_URL": "https://ultron.internal.example.com",
|
|
257
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_KEY": "wingsTokenKey",
|
|
258
|
-
"QINGFLOW_MCP_CREDIT_WINGS_TOKEN_VALUE": "replace-prod-balance-token",
|
|
259
232
|
"QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
|
|
260
|
-
"
|
|
261
|
-
"QINGFLOW_MCP_CREDIT_APAAS_TOKEN_VALUE": "729ed3cc-8eea-11ec-b585-52540009137b",
|
|
262
|
-
"QINGFLOW_MCP_CREDIT_WS_ID_HEADER_KEY": "wsId",
|
|
263
|
-
"QINGFLOW_MCP_CREDIT_APAAS_AMOUNT": "1"
|
|
233
|
+
"QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
|
|
264
234
|
}
|
|
265
235
|
}
|
|
266
236
|
}
|
|
@@ -271,7 +241,7 @@ qingflow-app-builder-mcp
|
|
|
271
241
|
- 源码目录 `npm install` 不会把命令加到全局 PATH;这种模式请用 `node ./npm/bin/qingflow.mjs`、`node ./npm/bin/qingflow-app-user-mcp.mjs` 或 `node ./npm/bin/qingflow-app-builder-mcp.mjs`
|
|
272
242
|
- `npx` 方式适合临时安装或容器化本地 agent
|
|
273
243
|
- 全局安装方式更适合长期固定使用的本机开发环境
|
|
274
|
-
-
|
|
244
|
+
- 计费接口使用当前登录会话的 `token` 与 `wsId` 请求头,可通过 `QINGFLOW_MCP_CREDIT_APAAS_BASE_URL/PATH` 覆盖调用记录接口地址
|
|
275
245
|
|
|
276
246
|
## 排障
|
|
277
247
|
|
|
@@ -328,5 +298,6 @@ qingflow auth use-credential \
|
|
|
328
298
|
|
|
329
299
|
说明:
|
|
330
300
|
|
|
331
|
-
-
|
|
301
|
+
- 本地会把解析后的 `token` 和原始 `credential` 写入 profile 文件,用于后续 CLI 命令恢复会话
|
|
302
|
+
- `persist=true` 时,本地还会优先把解析后的 `token` 和原始 `credential` 同步写入系统 keychain
|
|
332
303
|
- 当前工作区以 `/mcp/auth/context` 返回的 `wsId` 为准,不再通过本地 MCP 显式切换
|
package/package.json
CHANGED
|
@@ -21,13 +21,7 @@ DEFAULT_REPOSITORY_PROD_BRANCH = "prod"
|
|
|
21
21
|
DEFAULT_REPOSITORY_AUTHOR_NAME = "qingflow-mcp"
|
|
22
22
|
DEFAULT_REPOSITORY_AUTHOR_EMAIL = "qingflow-mcp@local.invalid"
|
|
23
23
|
DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY = "tokenKey"
|
|
24
|
-
|
|
25
|
-
DEFAULT_CREDIT_USAGE_RECORD_PATH = "/share/workspace/credit/usage/record"
|
|
26
|
-
DEFAULT_CREDIT_WINGS_TOKEN_HEADER_KEY = "wingsTokenKey"
|
|
27
|
-
DEFAULT_CREDIT_APAAS_TOKEN_HEADER_KEY = "serviceToken"
|
|
28
|
-
DEFAULT_CREDIT_WS_ID_HEADER_KEY = "wsId"
|
|
29
|
-
DEFAULT_CREDIT_USAGE_AMOUNT = "1"
|
|
30
|
-
DEFAULT_CREDIT_APAAS_TOKEN_VALUE = "729ed3cc-8eea-11ec-b585-52540009137b"
|
|
24
|
+
DEFAULT_CREDIT_USAGE_RECORD_PATH = "/user/credit/usage"
|
|
31
25
|
|
|
32
26
|
|
|
33
27
|
def get_mcp_home() -> Path:
|
|
@@ -225,80 +219,6 @@ def get_credit_meter_enabled() -> bool:
|
|
|
225
219
|
return normalized in {"1", "true", "yes", "on"}
|
|
226
220
|
|
|
227
221
|
|
|
228
|
-
def get_credit_shared_token_key() -> str:
|
|
229
|
-
value = get_config_value(
|
|
230
|
-
"credit_meter.shared_token_key",
|
|
231
|
-
env_var="QINGFLOW_MCP_CREDIT_TOKEN_KEY",
|
|
232
|
-
default=DEFAULT_CREDIT_APAAS_TOKEN_HEADER_KEY,
|
|
233
|
-
)
|
|
234
|
-
normalized = str(value or "").strip()
|
|
235
|
-
return normalized or DEFAULT_CREDIT_APAAS_TOKEN_HEADER_KEY
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def get_credit_shared_token_value() -> str | None:
|
|
239
|
-
value = get_config_value(
|
|
240
|
-
"credit_meter.shared_token_value",
|
|
241
|
-
env_var="QINGFLOW_MCP_CREDIT_TOKEN_VALUE",
|
|
242
|
-
default=None,
|
|
243
|
-
)
|
|
244
|
-
normalized = str(value or "").strip()
|
|
245
|
-
return normalized or None
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
def get_credit_shared_ws_id_header_key() -> str:
|
|
249
|
-
value = get_config_value(
|
|
250
|
-
"credit_meter.shared_ws_id_header_key",
|
|
251
|
-
env_var="QINGFLOW_MCP_CREDIT_WS_ID_HEADER_KEY",
|
|
252
|
-
default=DEFAULT_CREDIT_WS_ID_HEADER_KEY,
|
|
253
|
-
)
|
|
254
|
-
normalized = str(value or "").strip()
|
|
255
|
-
return normalized or DEFAULT_CREDIT_WS_ID_HEADER_KEY
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def get_credit_balance_base_url() -> str | None:
|
|
259
|
-
value = get_config_value(
|
|
260
|
-
"credit_meter.wings.base_url",
|
|
261
|
-
env_var="QINGFLOW_MCP_CREDIT_WINGS_BASE_URL",
|
|
262
|
-
default=None,
|
|
263
|
-
)
|
|
264
|
-
normalized = normalize_base_url(value)
|
|
265
|
-
return normalized or None
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
def get_credit_balance_path() -> str:
|
|
269
|
-
value = get_config_value(
|
|
270
|
-
"credit_meter.wings.path",
|
|
271
|
-
env_var="QINGFLOW_MCP_CREDIT_WINGS_PATH",
|
|
272
|
-
default=DEFAULT_CREDIT_BALANCE_PATH,
|
|
273
|
-
)
|
|
274
|
-
normalized = str(value or "").strip()
|
|
275
|
-
return normalized or DEFAULT_CREDIT_BALANCE_PATH
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def get_credit_balance_token_key() -> str:
|
|
279
|
-
value = get_config_value(
|
|
280
|
-
"credit_meter.wings.token_key",
|
|
281
|
-
env_var="QINGFLOW_MCP_CREDIT_WINGS_TOKEN_KEY",
|
|
282
|
-
default=DEFAULT_CREDIT_WINGS_TOKEN_HEADER_KEY,
|
|
283
|
-
)
|
|
284
|
-
normalized = str(value or "").strip()
|
|
285
|
-
return normalized or DEFAULT_CREDIT_WINGS_TOKEN_HEADER_KEY
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
def get_credit_balance_token_value() -> str | None:
|
|
289
|
-
value = get_config_value(
|
|
290
|
-
"credit_meter.wings.token_value",
|
|
291
|
-
env_var="QINGFLOW_MCP_CREDIT_WINGS_TOKEN_VALUE",
|
|
292
|
-
default=None,
|
|
293
|
-
)
|
|
294
|
-
normalized = str(value or "").strip()
|
|
295
|
-
return normalized or None
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def get_credit_balance_ws_id_header_key() -> str:
|
|
299
|
-
return get_credit_shared_ws_id_header_key()
|
|
300
|
-
|
|
301
|
-
|
|
302
222
|
def get_credit_usage_base_url() -> str | None:
|
|
303
223
|
value = get_config_value(
|
|
304
224
|
"credit_meter.apaas.base_url",
|
|
@@ -319,40 +239,6 @@ def get_credit_usage_path() -> str:
|
|
|
319
239
|
return normalized or DEFAULT_CREDIT_USAGE_RECORD_PATH
|
|
320
240
|
|
|
321
241
|
|
|
322
|
-
def get_credit_usage_token_key() -> str:
|
|
323
|
-
value = get_config_value(
|
|
324
|
-
"credit_meter.apaas.token_key",
|
|
325
|
-
env_var="QINGFLOW_MCP_CREDIT_APAAS_TOKEN_KEY",
|
|
326
|
-
default=DEFAULT_CREDIT_APAAS_TOKEN_HEADER_KEY,
|
|
327
|
-
)
|
|
328
|
-
normalized = str(value or "").strip()
|
|
329
|
-
return normalized or DEFAULT_CREDIT_APAAS_TOKEN_HEADER_KEY
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
def get_credit_usage_token_value() -> str | None:
|
|
333
|
-
value = get_config_value(
|
|
334
|
-
"credit_meter.apaas.token_value",
|
|
335
|
-
env_var="QINGFLOW_MCP_CREDIT_APAAS_TOKEN_VALUE",
|
|
336
|
-
default=DEFAULT_CREDIT_APAAS_TOKEN_VALUE,
|
|
337
|
-
)
|
|
338
|
-
normalized = str(value or "").strip()
|
|
339
|
-
return normalized or None
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def get_credit_usage_ws_id_header_key() -> str:
|
|
343
|
-
return get_credit_shared_ws_id_header_key()
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
def get_credit_usage_amount() -> str:
|
|
347
|
-
value = get_config_value(
|
|
348
|
-
"credit_meter.apaas.amount",
|
|
349
|
-
env_var="QINGFLOW_MCP_CREDIT_APAAS_AMOUNT",
|
|
350
|
-
default=DEFAULT_CREDIT_USAGE_AMOUNT,
|
|
351
|
-
)
|
|
352
|
-
normalized = str(value or "").strip()
|
|
353
|
-
return normalized or DEFAULT_CREDIT_USAGE_AMOUNT
|
|
354
|
-
|
|
355
|
-
|
|
356
242
|
def get_repository_default_group() -> str | None:
|
|
357
243
|
value = get_config_value(
|
|
358
244
|
"repository.default_group",
|
|
@@ -28,6 +28,9 @@ class SessionProfile:
|
|
|
28
28
|
base_url: str
|
|
29
29
|
qf_version: str | None
|
|
30
30
|
qf_version_source: str | None
|
|
31
|
+
token: str | None
|
|
32
|
+
login_token: str | None
|
|
33
|
+
credential: str | None
|
|
31
34
|
uid: int
|
|
32
35
|
email: str | None
|
|
33
36
|
nick_name: str | None
|
|
@@ -44,6 +47,9 @@ class SessionProfile:
|
|
|
44
47
|
base_url=value["base_url"],
|
|
45
48
|
qf_version=value.get("qf_version"),
|
|
46
49
|
qf_version_source=value.get("qf_version_source"),
|
|
50
|
+
token=value.get("token"),
|
|
51
|
+
login_token=value.get("login_token"),
|
|
52
|
+
credential=value.get("credential"),
|
|
47
53
|
uid=value["uid"],
|
|
48
54
|
email=value.get("email"),
|
|
49
55
|
nick_name=value.get("nick_name"),
|
|
@@ -112,6 +118,9 @@ class SessionStore:
|
|
|
112
118
|
base_url=normalize_base_url(base_url) or base_url,
|
|
113
119
|
qf_version=(str(qf_version).strip() or None) if qf_version is not None else None,
|
|
114
120
|
qf_version_source=(str(qf_version_source).strip() or None) if qf_version_source is not None else None,
|
|
121
|
+
token=str(token).strip() or None,
|
|
122
|
+
login_token=(str(login_token).strip() or None) if login_token is not None else None,
|
|
123
|
+
credential=(str(credential).strip() or None) if credential is not None else None,
|
|
115
124
|
uid=uid,
|
|
116
125
|
email=email,
|
|
117
126
|
nick_name=nick_name,
|
|
@@ -122,9 +131,9 @@ class SessionStore:
|
|
|
122
131
|
updated_at=now,
|
|
123
132
|
)
|
|
124
133
|
self._memory_sessions[profile] = BackendSession(
|
|
125
|
-
token=token,
|
|
126
|
-
login_token=login_token,
|
|
127
|
-
credential=
|
|
134
|
+
token=session_profile.token or token,
|
|
135
|
+
login_token=session_profile.login_token,
|
|
136
|
+
credential=session_profile.credential,
|
|
128
137
|
profile=profile,
|
|
129
138
|
base_url=session_profile.base_url,
|
|
130
139
|
qf_version=session_profile.qf_version,
|
|
@@ -152,15 +161,19 @@ class SessionStore:
|
|
|
152
161
|
memory_session.qf_version = session_profile.qf_version
|
|
153
162
|
memory_session.qf_version_source = session_profile.qf_version_source
|
|
154
163
|
return memory_session
|
|
155
|
-
if not session_profile
|
|
164
|
+
if not session_profile:
|
|
156
165
|
return None
|
|
157
|
-
token = self._get_secret(self._token_key(profile))
|
|
166
|
+
token = self._get_secret(self._token_key(profile)) if session_profile.persisted else None
|
|
167
|
+
if not token:
|
|
168
|
+
token = session_profile.token
|
|
158
169
|
if not token:
|
|
159
170
|
return None
|
|
171
|
+
login_token = self._get_secret(self._login_token_key(profile)) if session_profile.persisted else None
|
|
172
|
+
credential = self._get_secret(self._credential_key(profile)) if session_profile.persisted else None
|
|
160
173
|
backend_session = BackendSession(
|
|
161
174
|
token=token,
|
|
162
|
-
login_token=
|
|
163
|
-
credential=
|
|
175
|
+
login_token=login_token or session_profile.login_token,
|
|
176
|
+
credential=credential or session_profile.credential,
|
|
164
177
|
profile=profile,
|
|
165
178
|
base_url=session_profile.base_url,
|
|
166
179
|
qf_version=session_profile.qf_version,
|
|
@@ -110,7 +110,6 @@ class AuthTools(ToolBase):
|
|
|
110
110
|
uid = self._coerce_int(context_payload.get("uid"))
|
|
111
111
|
if uid is None:
|
|
112
112
|
raise_tool_error(QingflowApiError(category="auth", message="Credential context did not return valid user info"))
|
|
113
|
-
|
|
114
113
|
session_profile = self.sessions.save_session(
|
|
115
114
|
profile=profile,
|
|
116
115
|
base_url=resolved_base_url,
|
|
@@ -2,23 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
from contextvars import ContextVar
|
|
5
|
-
from decimal import Decimal, InvalidOperation
|
|
6
5
|
from typing import Any, Callable, TypeVar
|
|
7
6
|
|
|
8
|
-
from ..backend_client import BackendRequestContext, BackendClient
|
|
7
|
+
from ..backend_client import BackendRequestContext, BackendClient
|
|
9
8
|
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
9
|
get_credit_meter_enabled,
|
|
16
|
-
get_credit_usage_amount,
|
|
17
10
|
get_credit_usage_base_url,
|
|
18
11
|
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
12
|
)
|
|
23
13
|
from ..errors import QingflowApiError, raise_tool_error
|
|
24
14
|
from ..json_types import JSONObject
|
|
@@ -224,143 +214,42 @@ class ToolBase:
|
|
|
224
214
|
) -> None:
|
|
225
215
|
if not get_credit_meter_enabled():
|
|
226
216
|
return
|
|
227
|
-
if not self._credit_meter_is_ready():
|
|
228
|
-
return
|
|
229
217
|
if context.ws_id is None:
|
|
230
218
|
raise_tool_error(QingflowApiError(category="payment", message="credit meter requires wsId in current session context"))
|
|
231
219
|
|
|
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
220
|
self._record_credit_usage(
|
|
242
221
|
tool_name=tool_name,
|
|
243
|
-
|
|
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
|
-
},
|
|
222
|
+
context=context,
|
|
298
223
|
)
|
|
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
224
|
|
|
314
225
|
def _record_credit_usage(
|
|
315
226
|
self,
|
|
316
227
|
*,
|
|
317
228
|
tool_name: str,
|
|
318
|
-
|
|
319
|
-
uid: int,
|
|
229
|
+
context: BackendRequestContext,
|
|
320
230
|
) -> None:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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(
|
|
231
|
+
usage_context = BackendRequestContext(
|
|
232
|
+
base_url=get_credit_usage_base_url() or context.base_url,
|
|
233
|
+
token=context.token,
|
|
234
|
+
ws_id=context.ws_id,
|
|
235
|
+
qf_request_id=context.qf_request_id,
|
|
236
|
+
qf_version=context.qf_version,
|
|
237
|
+
qf_version_source=context.qf_version_source,
|
|
238
|
+
)
|
|
239
|
+
self.backend.request(
|
|
338
240
|
"POST",
|
|
339
|
-
|
|
241
|
+
usage_context,
|
|
340
242
|
get_credit_usage_path(),
|
|
341
|
-
headers={
|
|
342
|
-
usage_token_key: usage_token_value,
|
|
343
|
-
ws_id_header_key: str(ws_id),
|
|
344
|
-
},
|
|
345
243
|
json_body={
|
|
346
|
-
"
|
|
347
|
-
"
|
|
348
|
-
"
|
|
349
|
-
"businessType": "WORKSPACE",
|
|
244
|
+
"skuType": "MCP",
|
|
245
|
+
"skuName": "MCP",
|
|
246
|
+
"modelName": "MCP",
|
|
350
247
|
"scene": "MCP",
|
|
351
|
-
"aiBiz": "
|
|
248
|
+
"aiBiz": "MCP",
|
|
352
249
|
"extraInfo": tool_name,
|
|
353
250
|
},
|
|
354
251
|
)
|
|
355
252
|
|
|
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
253
|
def _require_dict(self, payload: JSONObject | None, field_name: str = "payload") -> JSONObject:
|
|
365
254
|
if not isinstance(payload, dict) or not payload:
|
|
366
255
|
raise_tool_error(QingflowApiError.config_error(f"{field_name} must be a non-empty object"))
|