@qingflow-tech/qingflow-app-user-mcp 1.0.0 → 1.0.2

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.2
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.2 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.2",
4
4
  "description": "Operational end-user MCP for Qingflow records, tasks, comments, and directory workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,8 +25,13 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
25
25
  logout.add_argument("--forget-persisted", action="store_true")
26
26
  logout.set_defaults(handler=_handle_logout, format_hint="")
27
27
 
28
+
28
29
  def _handle_use_credential(args: argparse.Namespace, context: CliContext) -> dict:
29
- credential = read_secret_arg(args.credential, stdin_enabled=bool(args.credential_stdin), label="credential")
30
+ credential = (
31
+ read_secret_arg(args.credential, stdin_enabled=bool(args.credential_stdin), label="credential")
32
+ if args.credential or bool(args.credential_stdin)
33
+ else ""
34
+ )
30
35
  return context.auth.auth_use_credential(
31
36
  profile=args.profile,
32
37
  base_url=args.base_url,
@@ -21,13 +21,8 @@ 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"
25
+ DEFAULT_MCPORTER_CONFIG_PATH = "/root/.openclaw/workspace/config/mcporter.json"
31
26
 
32
27
 
33
28
  def get_mcp_home() -> Path:
@@ -39,6 +34,13 @@ def get_profiles_path() -> Path:
39
34
  return get_mcp_home() / "profiles.json"
40
35
 
41
36
 
37
+ def get_mcporter_config_path() -> Path:
38
+ custom_path = os.getenv("QINGFLOW_MCP_MCPORTER_CONFIG_PATH") or os.getenv(
39
+ "QINGFLOW_MCP_AUTH_CONFIG_PATH"
40
+ )
41
+ return Path(custom_path).expanduser() if custom_path else Path(DEFAULT_MCPORTER_CONFIG_PATH)
42
+
43
+
42
44
  def get_repository_metadata_dir() -> Path:
43
45
  return get_mcp_home() / "repository-metadata"
44
46
 
@@ -225,80 +227,6 @@ def get_credit_meter_enabled() -> bool:
225
227
  return normalized in {"1", "true", "yes", "on"}
226
228
 
227
229
 
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
230
  def get_credit_usage_base_url() -> str | None:
303
231
  value = get_config_value(
304
232
  "credit_meter.apaas.base_url",
@@ -319,40 +247,6 @@ def get_credit_usage_path() -> str:
319
247
  return normalized or DEFAULT_CREDIT_USAGE_RECORD_PATH
320
248
 
321
249
 
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
250
  def get_repository_default_group() -> str | None:
357
251
  value = get_config_value(
358
252
  "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,
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import json
3
4
  from typing import Any
4
5
 
5
6
  from mcp.server.fastmcp import FastMCP
@@ -9,6 +10,7 @@ from ..config import (
9
10
  DEFAULT_PROFILE,
10
11
  get_default_base_url,
11
12
  get_default_qf_version,
13
+ get_mcporter_config_path,
12
14
  normalize_base_url,
13
15
  )
14
16
  from ..errors import QingflowApiError, raise_tool_error
@@ -77,15 +79,24 @@ class AuthTools(ToolBase):
77
79
  profile: str = DEFAULT_PROFILE,
78
80
  base_url: str | None = None,
79
81
  qf_version: str | None = None,
80
- credential: str,
82
+ credential: str | None = None,
81
83
  persist: bool = False,
82
84
  ) -> dict[str, Any]:
83
85
  """执行认证与会话相关逻辑。"""
84
- normalized_base_url = self._normalize_base_url(base_url)
86
+ resolved_base_url, resolved_credential = self._resolve_mcporter_auth_inputs(
87
+ base_url=base_url,
88
+ credential=credential,
89
+ )
90
+ normalized_base_url = self._normalize_base_url(resolved_base_url)
85
91
  normalized_qf_version, qf_version_source = self._resolve_qf_version_input(qf_version)
86
- normalized_credential = str(credential).strip()
92
+ normalized_credential = str(resolved_credential).strip()
87
93
  if not normalized_credential:
88
- raise_tool_error(QingflowApiError.config_error("credential is required"))
94
+ raise_tool_error(
95
+ QingflowApiError.config_error(
96
+ "credential is required or configure /root/.openclaw/workspace/config/mcporter.json "
97
+ "with mcpServers.qingflow.headers.x-qingflow-client-id"
98
+ )
99
+ )
89
100
 
90
101
  context_payload, detected_qf_version = self._fetch_auth_context(
91
102
  normalized_base_url,
@@ -110,7 +121,6 @@ class AuthTools(ToolBase):
110
121
  uid = self._coerce_int(context_payload.get("uid"))
111
122
  if uid is None:
112
123
  raise_tool_error(QingflowApiError(category="auth", message="Credential context did not return valid user info"))
113
-
114
124
  session_profile = self.sessions.save_session(
115
125
  profile=profile,
116
126
  base_url=resolved_base_url,
@@ -154,6 +164,46 @@ class AuthTools(ToolBase):
154
164
  ),
155
165
  }
156
166
 
167
+ def _resolve_mcporter_auth_inputs(self, *, base_url: str | None, credential: str | None) -> tuple[str | None, str]:
168
+ """从参数或 mcporter 配置解析登录所需 base_url 与 credential。"""
169
+ normalized_base_url = self._normalize_text(base_url)
170
+ normalized_credential = self._normalize_text(credential)
171
+ if normalized_base_url and normalized_credential:
172
+ return normalized_base_url, normalized_credential
173
+
174
+ mcporter_context = self._read_mcporter_qingflow_context()
175
+ if not normalized_base_url:
176
+ normalized_base_url = self._normalize_text(mcporter_context.get("base_url"))
177
+ if not normalized_credential:
178
+ normalized_credential = self._normalize_text(mcporter_context.get("credential"))
179
+ return normalized_base_url, normalized_credential or ""
180
+
181
+ def _read_mcporter_qingflow_context(self) -> dict[str, str]:
182
+ """读取 OpenClaw mcporter 中的 Qingflow MCP 上下文。"""
183
+ path = get_mcporter_config_path()
184
+ if not path.exists():
185
+ return {}
186
+ try:
187
+ with path.open("r", encoding="utf-8") as handle:
188
+ payload = json.load(handle)
189
+ except (OSError, json.JSONDecodeError) as exc:
190
+ raise_tool_error(QingflowApiError.config_error(f"failed to read mcporter config '{path}': {exc}"))
191
+
192
+ if not isinstance(payload, dict):
193
+ raise_tool_error(QingflowApiError.config_error(f"mcporter config '{path}' must be a JSON object"))
194
+ mcp_servers = payload.get("mcpServers")
195
+ qingflow = mcp_servers.get("qingflow") if isinstance(mcp_servers, dict) else None
196
+ if not isinstance(qingflow, dict):
197
+ return {}
198
+ headers = qingflow.get("headers")
199
+ credential = None
200
+ if isinstance(headers, dict):
201
+ credential = headers.get("x-qingflow-client-id")
202
+ return {
203
+ "base_url": str(qingflow.get("url") or "").strip(),
204
+ "credential": str(credential or "").strip(),
205
+ }
206
+
157
207
  @tool_cn_name("我的身份")
158
208
  def auth_whoami(self, *, profile: str = DEFAULT_PROFILE) -> dict[str, Any]:
159
209
  """执行认证与会话相关逻辑。"""
@@ -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,