@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 +2 -2
- package/docs/local-agent-install.md +8 -37
- package/package.json +1 -1
- package/src/qingflow_mcp/cli/commands/auth.py +6 -1
- package/src/qingflow_mcp/config.py +9 -115
- package/src/qingflow_mcp/session_store.py +20 -7
- package/src/qingflow_mcp/tools/auth_tools.py +55 -5
- 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-user-mcp@1.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.
|
|
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
|
-
"
|
|
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
|
@@ -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 =
|
|
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
|
-
|
|
25
|
-
|
|
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=
|
|
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,
|
|
@@ -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
|
-
|
|
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(
|
|
92
|
+
normalized_credential = str(resolved_credential).strip()
|
|
87
93
|
if not normalized_credential:
|
|
88
|
-
raise_tool_error(
|
|
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
|
|
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"))
|