@qingflow-tech/qingflow-app-user-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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-app-user-mcp@1.0.0
6
+ npm install @qingflow-tech/qingflow-app-user-mcp@1.0.1
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.0 qingflow-app-user-mcp
12
+ npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.1 qingflow-app-user-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
- "QINGFLOW_MCP_CREDIT_APAAS_TOKEN_KEY": "serviceToken",
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
- "QINGFLOW_MCP_CREDIT_APAAS_TOKEN_KEY": "serviceToken",
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
- "QINGFLOW_MCP_CREDIT_APAAS_TOKEN_KEY": "serviceToken",
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
- "QINGFLOW_MCP_CREDIT_APAAS_TOKEN_KEY": "serviceToken",
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
- "QINGFLOW_MCP_CREDIT_APAAS_TOKEN_KEY": "serviceToken",
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
- - 计费接口鉴权参数需要分系统配置:`QINGFLOW_MCP_CREDIT_WINGS_TOKEN_KEY/VALUE`(wings)与 `QINGFLOW_MCP_CREDIT_APAAS_TOKEN_KEY/VALUE`(apaas)互不复用
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
- - `persist=true` 时,本地会优先把解析后的 `token` 和原始 `credential` 写入系统 keychain
301
+ - 本地会把解析后的 `token` 和原始 `credential` 写入 profile 文件,用于后续 CLI 命令恢复会话
302
+ - `persist=true` 时,本地还会优先把解析后的 `token` 和原始 `credential` 同步写入系统 keychain
332
303
  - 当前工作区以 `/mcp/auth/context` 返回的 `wsId` 为准,不再通过本地 MCP 显式切换
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-app-user-mcp",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- DEFAULT_CREDIT_BALANCE_PATH = "/ultron/internal/credit/balance"
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=(str(credential).strip() or None) if credential is not None else None,
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 or not session_profile.persisted:
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=self._get_secret(self._login_token_key(profile)),
163
- credential=self._get_secret(self._credential_key(profile)),
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, BackendResponse
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
- 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
- },
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
- ws_id: int,
319
- uid: int,
229
+ context: BackendRequestContext,
320
230
  ) -> 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(
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
- usage_base_url,
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
- "wsId": ws_id,
347
- "uid": uid,
348
- "creditUsage": "1",
349
- "businessType": "WORKSPACE",
244
+ "skuType": "MCP",
245
+ "skuName": "MCP",
246
+ "modelName": "MCP",
350
247
  "scene": "MCP",
351
- "aiBiz": "mcp",
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"))
@@ -365,7 +365,6 @@ class TaskTools(ToolBase):
365
365
  """执行任务相关逻辑。"""
366
366
  self._validate_type(type)
367
367
  self._validate_process_status(process_status)
368
-
369
368
  def runner(session_profile, context):
370
369
  payload: dict[str, Any] = {
371
370
  "type": type,