@josephyan/qingflow-cli 0.2.0-beta.984 → 0.2.0-beta.986

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.
Files changed (43) hide show
  1. package/README.md +2 -2
  2. package/docs/local-agent-install.md +70 -11
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/src/qingflow_mcp/__init__.py +1 -1
  6. package/src/qingflow_mcp/builder_facade/service.py +47 -21
  7. package/src/qingflow_mcp/cli/commands/auth.py +14 -43
  8. package/src/qingflow_mcp/cli/commands/task.py +4 -1
  9. package/src/qingflow_mcp/cli/commands/workspace.py +0 -8
  10. package/src/qingflow_mcp/cli/formatters.py +0 -21
  11. package/src/qingflow_mcp/config.py +39 -0
  12. package/src/qingflow_mcp/errors.py +2 -2
  13. package/src/qingflow_mcp/public_surface.py +2 -6
  14. package/src/qingflow_mcp/response_trim.py +1 -8
  15. package/src/qingflow_mcp/server.py +1 -1
  16. package/src/qingflow_mcp/server_app_builder.py +4 -28
  17. package/src/qingflow_mcp/server_app_user.py +4 -28
  18. package/src/qingflow_mcp/session_store.py +31 -5
  19. package/src/qingflow_mcp/tools/ai_builder_tools.py +117 -1
  20. package/src/qingflow_mcp/tools/app_tools.py +51 -1
  21. package/src/qingflow_mcp/tools/approval_tools.py +82 -1
  22. package/src/qingflow_mcp/tools/auth_tools.py +258 -288
  23. package/src/qingflow_mcp/tools/base.py +204 -4
  24. package/src/qingflow_mcp/tools/code_block_tools.py +21 -0
  25. package/src/qingflow_mcp/tools/custom_button_tools.py +24 -1
  26. package/src/qingflow_mcp/tools/directory_tools.py +28 -1
  27. package/src/qingflow_mcp/tools/feedback_tools.py +8 -0
  28. package/src/qingflow_mcp/tools/file_tools.py +25 -1
  29. package/src/qingflow_mcp/tools/import_tools.py +40 -1
  30. package/src/qingflow_mcp/tools/navigation_tools.py +34 -1
  31. package/src/qingflow_mcp/tools/package_tools.py +37 -1
  32. package/src/qingflow_mcp/tools/portal_tools.py +28 -1
  33. package/src/qingflow_mcp/tools/qingbi_report_tools.py +38 -1
  34. package/src/qingflow_mcp/tools/record_tools.py +255 -2
  35. package/src/qingflow_mcp/tools/repository_dev_tools.py +21 -2
  36. package/src/qingflow_mcp/tools/resource_read_tools.py +23 -1
  37. package/src/qingflow_mcp/tools/role_tools.py +19 -1
  38. package/src/qingflow_mcp/tools/solution_tools.py +56 -1
  39. package/src/qingflow_mcp/tools/task_context_tools.py +205 -6
  40. package/src/qingflow_mcp/tools/task_tools.py +49 -3
  41. package/src/qingflow_mcp/tools/view_tools.py +56 -1
  42. package/src/qingflow_mcp/tools/workflow_tools.py +65 -1
  43. package/src/qingflow_mcp/tools/workspace_tools.py +14 -225
package/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.984
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.986
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.984 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.986 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
@@ -6,6 +6,20 @@
6
6
  2. 记录/待办优先的 `qingflow-app-user-mcp`
7
7
  3. 精简 builder 的 `qingflow-app-builder-mcp`
8
8
 
9
+ ## 本地鉴权推荐方案
10
+
11
+ 本地模式现在推荐优先使用 `credential` 建立会话,而不是直接注入 `token`。
12
+
13
+ 推荐链路:
14
+
15
+ 1. createClaw 或其它本地宿主为当前实例保存 `credential`
16
+ 2. 本地 MCP 调用 `auth_use_credential`
17
+ 3. MCP 用该 `credential` 请求 apaas `/mcp/auth/context`
18
+ 4. 解析并保存 `token / wsId / qfVersion / uid`
19
+ 5. 业务工具直接使用这份上下文
20
+
21
+ `auth_use_credential` 是本地唯一鉴权主路径。
22
+
9
23
  ## npm 安装器适用场景
10
24
 
11
25
  适合这类本地 agent / gateway:
@@ -63,17 +77,17 @@ npm run pack:npm
63
77
  会生成:
64
78
 
65
79
  ```bash
66
- dist/npm/josephyan-qingflow-cli-<version>.tgz
67
- dist/npm/josephyan-qingflow-app-user-mcp-<version>.tgz
68
- dist/npm/josephyan-qingflow-app-builder-mcp-<version>.tgz
80
+ dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
81
+ dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
82
+ dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
69
83
  ```
70
84
 
71
85
  然后在目标机器安装:
72
86
 
73
87
  ```bash
74
- npm install /absolute/path/to/dist/npm/josephyan-qingflow-cli-<version>.tgz
75
- npm install /absolute/path/to/dist/npm/josephyan-qingflow-app-user-mcp-<version>.tgz
76
- npm install /absolute/path/to/dist/npm/josephyan-qingflow-app-builder-mcp-<version>.tgz
88
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-cli-<version>.tgz
89
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-user-mcp-<version>.tgz
90
+ npm install /absolute/path/to/dist/npm/qingflow-tech-qingflow-app-builder-mcp-<version>.tgz
77
91
  ```
78
92
 
79
93
  安装时会自动:
@@ -136,7 +150,10 @@ qingflow-app-builder-mcp
136
150
  ],
137
151
  "env": {
138
152
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
139
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
153
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
154
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
155
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
156
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
140
157
  }
141
158
  }
142
159
  }
@@ -153,7 +170,10 @@ qingflow-app-builder-mcp
153
170
  "args": [],
154
171
  "env": {
155
172
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
156
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
173
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
174
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
175
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
176
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
157
177
  }
158
178
  }
159
179
  }
@@ -170,7 +190,10 @@ qingflow-app-builder-mcp
170
190
  "args": [],
171
191
  "env": {
172
192
  "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
173
- "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp"
193
+ "QINGFLOW_MCP_HOME": "/absolute/path/to/.qingflow-mcp",
194
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
195
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
196
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
174
197
  }
175
198
  }
176
199
  }
@@ -191,7 +214,10 @@ qingflow-app-builder-mcp
191
214
  "@josephyan/qingflow-app-user-mcp"
192
215
  ],
193
216
  "env": {
194
- "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api"
217
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
218
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
219
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
220
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
195
221
  }
196
222
  },
197
223
  "qingflow-builder": {
@@ -201,7 +227,10 @@ qingflow-app-builder-mcp
201
227
  "@josephyan/qingflow-app-builder-mcp"
202
228
  ],
203
229
  "env": {
204
- "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api"
230
+ "QINGFLOW_MCP_DEFAULT_BASE_URL": "https://qingflow.com/api",
231
+ "QINGFLOW_MCP_CREDIT_METER_ENABLED": "true",
232
+ "QINGFLOW_MCP_CREDIT_APAAS_BASE_URL": "https://apaas.internal.example.com",
233
+ "QINGFLOW_MCP_CREDIT_APAAS_PATH": "/user/credit/usage"
205
234
  }
206
235
  }
207
236
  }
@@ -212,6 +241,7 @@ qingflow-app-builder-mcp
212
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`
213
242
  - `npx` 方式适合临时安装或容器化本地 agent
214
243
  - 全局安装方式更适合长期固定使用的本机开发环境
244
+ - 计费接口使用当前登录会话的 `token` 与 `wsId` 请求头,可通过 `QINGFLOW_MCP_CREDIT_APAAS_BASE_URL/PATH` 覆盖调用记录接口地址
215
245
 
216
246
  ## 排障
217
247
 
@@ -242,3 +272,32 @@ npm install
242
272
  4. 再启动 MCP 客户端
243
273
 
244
274
  现在 stdio MCP 入口会拒绝在启动瞬间“边启动边重建 Python 运行时”,因为安装日志一旦写进 stdout,就会破坏 MCP 握手并表现成 `Transport closed`。如果运行时缺失或版本不一致,入口会直接报错并提示重装,而不是静默自修复。
275
+
276
+ ## createClaw 本地接入示例
277
+
278
+ 如果 createClaw 已经为当前本地实例保存了 `credential`,推荐在首次建链时调用:
279
+
280
+ ```bash
281
+ qingflow auth use-credential \
282
+ --base-url https://qingflow.com/api \
283
+ --credential-stdin
284
+ ```
285
+
286
+ 然后把 `credential` 写到 stdin。
287
+
288
+ 等价 MCP 工具调用参数:
289
+
290
+ ```json
291
+ {
292
+ "profile": "default",
293
+ "base_url": "https://qingflow.com/api",
294
+ "credential": "1602853_277941",
295
+ "persist": false
296
+ }
297
+ ```
298
+
299
+ 说明:
300
+
301
+ - 本地会把解析后的 `token` 和原始 `credential` 写入 profile 文件,用于后续 CLI 命令恢复会话
302
+ - `persist=true` 时,本地还会优先把解析后的 `token` 和原始 `credential` 同步写入系统 keychain
303
+ - 当前工作区以 `/mcp/auth/context` 返回的 `wsId` 为准,不再通过本地 MCP 显式切换
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.984",
3
+ "version": "0.2.0-beta.986",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b984"
7
+ version = "0.2.0b986"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
 
6
6
  __all__ = ["__version__"]
7
7
 
8
- _FALLBACK_VERSION = "0.2.0b984"
8
+ _FALLBACK_VERSION = "0.2.0b986"
9
9
 
10
10
 
11
11
  def _resolve_local_pyproject_version() -> str | None:
@@ -514,6 +514,8 @@ class AiBuilderFacade:
514
514
  }
515
515
  effective_package_id = _coerce_positive_int(package_id)
516
516
  created = False
517
+ create_result: JSONObject | None = None
518
+ update_result: JSONObject | None = None
517
519
  permission_outcomes: list[PermissionCheckOutcome] = []
518
520
 
519
521
  if effective_package_id is None:
@@ -604,11 +606,36 @@ class AiBuilderFacade:
604
606
  )
605
607
  except VisibilityResolutionError:
606
608
  expected_visibility = None
609
+ metadata_verified = True
610
+ if metadata_requested and update_result is not None:
611
+ metadata_verified = bool(update_result.get("verified"))
612
+ elif created and create_result is not None:
613
+ metadata_verified = bool(create_result.get("verified"))
614
+ layout_verified = True
615
+ if items is not None and layout_result is not None:
616
+ layout_verified = bool(layout_result.get("verified"))
617
+ response_verification: JSONObject = {
618
+ "package_exists": True,
619
+ "package_created": created,
620
+ "layout_applied": items is not None,
621
+ "metadata_verified": metadata_verified,
622
+ "layout_verified": layout_verified,
623
+ "visibility_verified": None
624
+ if expected_visibility is None
625
+ else _visibility_matches_expected(verification.get("visibility"), expected_visibility),
626
+ }
627
+ if isinstance(update_result, dict):
628
+ update_verification = update_result.get("verification")
629
+ if isinstance(update_verification, dict):
630
+ for key in ("package_name_verified", "package_icon_verified", "visibility_verified"):
631
+ if key in update_verification:
632
+ response_verification[key] = deepcopy(update_verification.get(key))
633
+ response_verified = metadata_verified and layout_verified and response_verification.get("visibility_verified") is not False
607
634
  response: JSONObject = {
608
- "status": "success",
635
+ "status": "success" if response_verified else "partial_success",
609
636
  "error_code": None,
610
637
  "recoverable": False,
611
- "message": "applied package",
638
+ "message": "applied package" if response_verified else "applied package with unverified readback",
612
639
  "normalized_args": normalized_args,
613
640
  "missing_fields": [],
614
641
  "allowed_values": {},
@@ -617,15 +644,8 @@ class AiBuilderFacade:
617
644
  "suggested_next_call": None,
618
645
  "noop": not (created or metadata_requested or items is not None),
619
646
  "warnings": [],
620
- "verification": {
621
- "package_exists": True,
622
- "package_created": created,
623
- "layout_applied": items is not None,
624
- "visibility_verified": None
625
- if expected_visibility is None
626
- else _visibility_matches_expected(verification.get("visibility"), expected_visibility),
627
- },
628
- "verified": True,
647
+ "verification": response_verification,
648
+ "verified": response_verified,
629
649
  **{
630
650
  key: deepcopy(value)
631
651
  for key, value in verification.items()
@@ -683,7 +703,7 @@ class AiBuilderFacade:
683
703
  )
684
704
  raw_current = current.get("result") if isinstance(current.get("result"), dict) else {}
685
705
  raw_current_base = current_base.get("result") if isinstance(current_base.get("result"), dict) else {}
686
- current_name = str(raw_current.get("tagName") or "").strip() or None
706
+ current_name = str(raw_current.get("tagName") or raw_current_base.get("tagName") or "").strip() or None
687
707
  desired_name = str(package_name or current_name or "").strip() or current_name or "未命名应用包"
688
708
  desired_icon = encode_workspace_icon_with_defaults(
689
709
  icon=icon,
@@ -724,27 +744,33 @@ class AiBuilderFacade:
724
744
  verification = self.package_get(profile=profile, package_id=tag_id)
725
745
  if verification.get("status") != "success":
726
746
  return verification
747
+ package_name_verified = str(verification.get("package_name") or "").strip() == desired_name
748
+ package_icon_verified = str(verification.get("icon") or "").strip() == desired_icon
749
+ visibility_verified = _visibility_matches_expected(
750
+ verification.get("visibility"),
751
+ _public_visibility_from_member_auth(desired_auth),
752
+ )
753
+ verified = package_name_verified and package_icon_verified and visibility_verified
727
754
  return {
728
- "status": "success",
755
+ "status": "success" if verified else "partial_success",
729
756
  "error_code": None,
730
757
  "recoverable": False,
731
- "message": "updated package",
758
+ "message": "updated package" if verified else "updated package with unverified readback",
732
759
  "normalized_args": normalized_args,
733
760
  "missing_fields": [],
734
761
  "allowed_values": {},
735
762
  "details": {},
736
763
  "request_id": update_result.get("request_id") if isinstance(update_result, dict) else None,
737
- "suggested_next_call": None,
764
+ "suggested_next_call": None if verified else {"tool_name": "package_get", "arguments": {"profile": profile, "package_id": tag_id}},
738
765
  "noop": False,
739
766
  "warnings": [],
740
767
  "verification": {
741
768
  "package_exists": True,
742
- "visibility_verified": _visibility_matches_expected(
743
- verification.get("visibility"),
744
- _public_visibility_from_member_auth(desired_auth),
745
- ),
769
+ "package_name_verified": package_name_verified,
770
+ "package_icon_verified": package_icon_verified,
771
+ "visibility_verified": visibility_verified,
746
772
  },
747
- "verified": True,
773
+ "verified": verified,
748
774
  **{
749
775
  key: deepcopy(value)
750
776
  for key, value in verification.items()
@@ -7682,7 +7708,7 @@ class AiBuilderFacade:
7682
7708
  "request_route": request_route,
7683
7709
  },
7684
7710
  suggested_next_call=(
7685
- {"tool_name": "workspace_select", "arguments": {"profile": profile, "ws_id": request_route.get("ws_id")}}
7711
+ {"tool_name": "auth_use_credential", "arguments": {"profile": profile}}
7686
7712
  if request_route.get("ws_id")
7687
7713
  else None
7688
7714
  ),
@@ -1,10 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
- import getpass
5
- import sys
6
4
 
7
- from ...errors import QingflowApiError
8
5
  from ..context import CliContext
9
6
  from .common import read_secret_arg
10
7
 
@@ -13,23 +10,13 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
13
10
  parser = subparsers.add_parser("auth", help="认证与会话")
14
11
  auth_subparsers = parser.add_subparsers(dest="auth_command", required=True)
15
12
 
16
- login = auth_subparsers.add_parser("login", help="邮箱密码登录")
17
- login.add_argument("--base-url")
18
- login.add_argument("--qf-version")
19
- login.add_argument("--email", required=True)
20
- login.add_argument("--password")
21
- login.add_argument("--password-stdin", action="store_true")
22
- login.add_argument("--persist", action=argparse.BooleanOptionalAction, default=True)
23
- login.set_defaults(handler=_handle_login, format_hint="auth_whoami")
24
-
25
- use_token = auth_subparsers.add_parser("use-token", help="直接注入 token")
26
- use_token.add_argument("--base-url")
27
- use_token.add_argument("--qf-version")
28
- use_token.add_argument("--token")
29
- use_token.add_argument("--token-stdin", action="store_true")
30
- use_token.add_argument("--ws-id", type=int)
31
- use_token.add_argument("--persist", action=argparse.BooleanOptionalAction, default=False)
32
- use_token.set_defaults(handler=_handle_use_token, format_hint="auth_whoami")
13
+ use_credential = auth_subparsers.add_parser("use-credential", help="直接注入 credential")
14
+ use_credential.add_argument("--base-url")
15
+ use_credential.add_argument("--qf-version")
16
+ use_credential.add_argument("--credential")
17
+ use_credential.add_argument("--credential-stdin", action="store_true")
18
+ use_credential.add_argument("--persist", action=argparse.BooleanOptionalAction, default=False)
19
+ use_credential.set_defaults(handler=_handle_use_credential, format_hint="auth_whoami")
33
20
 
34
21
  whoami = auth_subparsers.add_parser("whoami", help="查看当前登录态")
35
22
  whoami.set_defaults(handler=_handle_whoami, format_hint="auth_whoami")
@@ -39,33 +26,17 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
39
26
  logout.set_defaults(handler=_handle_logout, format_hint="")
40
27
 
41
28
 
42
- def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
43
- password = args.password
44
- if args.password_stdin:
45
- password = read_secret_arg(args.password, stdin_enabled=True, label="password")
46
- elif not password:
47
- if sys.stdin.isatty():
48
- password = getpass.getpass("Password: ")
49
- else:
50
- raise QingflowApiError.config_error("password is required; use --password or --password-stdin")
51
- return context.auth.auth_login(
52
- profile=args.profile,
53
- base_url=args.base_url,
54
- qf_version=args.qf_version,
55
- email=args.email,
56
- password=password,
57
- persist=bool(args.persist),
29
+ def _handle_use_credential(args: argparse.Namespace, context: CliContext) -> dict:
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 ""
58
34
  )
59
-
60
-
61
- def _handle_use_token(args: argparse.Namespace, context: CliContext) -> dict:
62
- token = read_secret_arg(args.token, stdin_enabled=bool(args.token_stdin), label="token")
63
- return context.auth.auth_use_token(
35
+ return context.auth.auth_use_credential(
64
36
  profile=args.profile,
65
37
  base_url=args.base_url,
66
38
  qf_version=args.qf_version,
67
- token=token,
68
- ws_id=args.ws_id,
39
+ credential=credential,
69
40
  persist=bool(args.persist),
70
41
  )
71
42
 
@@ -15,7 +15,10 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
15
15
  list_parser.add_argument("--flow-status", default="all")
16
16
  list_parser.add_argument("--app-key")
17
17
  list_parser.add_argument("--workflow-node-id", type=int)
18
- list_parser.add_argument("--query")
18
+ list_parser.add_argument(
19
+ "--query",
20
+ help="先走后端待办检索;当后端返回零结果时,公开 task_list 会回退到本地匹配 app_name / workflow_node_name / app_key / record_id。",
21
+ )
19
22
  list_parser.add_argument("--page", type=int, default=1)
20
23
  list_parser.add_argument("--page-size", type=int, default=20)
21
24
  list_parser.set_defaults(handler=_handle_list, format_hint="task_list")
@@ -15,10 +15,6 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
15
15
  list_parser.add_argument("--include-external", action="store_true")
16
16
  list_parser.set_defaults(handler=_handle_list, format_hint="workspace_list")
17
17
 
18
- select = workspace_subparsers.add_parser("select", help="切换工作区")
19
- select.add_argument("--ws-id", type=int, required=True)
20
- select.set_defaults(handler=_handle_select, format_hint="workspace_select")
21
-
22
18
 
23
19
  def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
24
20
  return context.workspace.workspace_list(
@@ -27,7 +23,3 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
27
23
  page_size=args.page_size,
28
24
  include_external=bool(args.include_external),
29
25
  )
30
-
31
-
32
- def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
33
- return context.workspace.workspace_select(profile=args.profile, ws_id=args.ws_id)
@@ -132,26 +132,6 @@ def _format_app_get(result: dict[str, Any]) -> str:
132
132
  _append_warnings(lines, result.get("warnings"))
133
133
  return "\n".join(lines) + "\n"
134
134
 
135
-
136
- def _format_workspace_select(result: dict[str, Any]) -> str:
137
- lines = [
138
- f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
139
- f"QF Version: {result.get('qf_version') or '-'}",
140
- ]
141
- workspace_version = result.get("workspace_version") if isinstance(result.get("workspace_version"), dict) else {}
142
- if workspace_version:
143
- lines.append(
144
- "Workspace Version: "
145
- f"{workspace_version.get('display_name') or '-'} "
146
- f"({workspace_version.get('level_name') or workspace_version.get('level_code') or '-'})"
147
- )
148
- if workspace_version.get("being_trial") is not None:
149
- lines.append(f"Trial: {workspace_version.get('being_trial')}")
150
- if workspace_version.get("expire_date") is not None:
151
- lines.append(f"Expire Date: {workspace_version.get('expire_date')}")
152
- return "\n".join(lines) + "\n"
153
-
154
-
155
135
  def _format_record_list(result: dict[str, Any]) -> str:
156
136
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
157
137
  items = data.get("items") if isinstance(data.get("items"), list) else []
@@ -364,7 +344,6 @@ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
364
344
  _FORMATTERS = {
365
345
  "auth_whoami": _format_whoami,
366
346
  "workspace_list": _format_workspace_list,
367
- "workspace_select": _format_workspace_select,
368
347
  "app_list": _format_app_items,
369
348
  "app_search": _format_app_items,
370
349
  "app_get": _format_app_get,
@@ -21,6 +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_USAGE_RECORD_PATH = "/user/credit/usage"
25
+ DEFAULT_MCPORTER_CONFIG_PATH = "~/.openclaw/workspace/config/mcporter.json"
24
26
 
25
27
 
26
28
  def get_mcp_home() -> Path:
@@ -32,6 +34,13 @@ def get_profiles_path() -> Path:
32
34
  return get_mcp_home() / "profiles.json"
33
35
 
34
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
+
35
44
  def get_repository_metadata_dir() -> Path:
36
45
  return get_mcp_home() / "repository-metadata"
37
46
 
@@ -208,6 +217,36 @@ def get_log_level() -> str:
208
217
  )
209
218
 
210
219
 
220
+ def get_credit_meter_enabled() -> bool:
221
+ value = get_config_value(
222
+ "credit_meter.enabled",
223
+ env_var="QINGFLOW_MCP_CREDIT_METER_ENABLED",
224
+ default="true",
225
+ )
226
+ normalized = str(value or "").strip().lower()
227
+ return normalized in {"1", "true", "yes", "on"}
228
+
229
+
230
+ def get_credit_usage_base_url() -> str | None:
231
+ value = get_config_value(
232
+ "credit_meter.apaas.base_url",
233
+ env_var="QINGFLOW_MCP_CREDIT_APAAS_BASE_URL",
234
+ default=None,
235
+ )
236
+ normalized = normalize_base_url(value)
237
+ return normalized or None
238
+
239
+
240
+ def get_credit_usage_path() -> str:
241
+ value = get_config_value(
242
+ "credit_meter.apaas.path",
243
+ env_var="QINGFLOW_MCP_CREDIT_APAAS_PATH",
244
+ default=DEFAULT_CREDIT_USAGE_RECORD_PATH,
245
+ )
246
+ normalized = str(value or "").strip()
247
+ return normalized or DEFAULT_CREDIT_USAGE_RECORD_PATH
248
+
249
+
211
250
  def get_repository_default_group() -> str | None:
212
251
  value = get_config_value(
213
252
  "repository.default_group",
@@ -43,14 +43,14 @@ class QingflowApiError(Exception):
43
43
  def auth_required(cls, profile: str) -> "QingflowApiError":
44
44
  return cls(
45
45
  category="auth",
46
- message=f"Profile '{profile}' is not logged in. Run auth_login first.",
46
+ message=f"Profile '{profile}' is not logged in. Run auth_use_credential first.",
47
47
  )
48
48
 
49
49
  @classmethod
50
50
  def workspace_not_selected(cls, profile: str) -> "QingflowApiError":
51
51
  return cls(
52
52
  category="workspace",
53
- message=f"WORKSPACE_NOT_SELECTED: profile '{profile}' has no selected workspace. Run workspace_select first.",
53
+ message=f"WORKSPACE_NOT_SELECTED: profile '{profile}' has no workspace from auth context. Re-run auth_use_credential.",
54
54
  )
55
55
 
56
56
  @classmethod
@@ -30,12 +30,10 @@ def tool_key(domain: str, tool_name: str) -> str:
30
30
 
31
31
 
32
32
  USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
33
- PublicToolSpec(USER_DOMAIN, "auth_login", ("auth_login",), ("auth", "login")),
34
- PublicToolSpec(USER_DOMAIN, "auth_use_token", ("auth_use_token",), ("auth", "use-token")),
33
+ PublicToolSpec(USER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("auth", "use-credential")),
35
34
  PublicToolSpec(USER_DOMAIN, "auth_whoami", ("auth_whoami",), ("auth", "whoami")),
36
35
  PublicToolSpec(USER_DOMAIN, "auth_logout", ("auth_logout",), ("auth", "logout")),
37
36
  PublicToolSpec(USER_DOMAIN, "workspace_list", ("workspace_list",), ("workspace", "list")),
38
- PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
39
37
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
40
38
  PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
41
39
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
@@ -107,12 +105,10 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
107
105
 
108
106
 
109
107
  BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
110
- PublicToolSpec(BUILDER_DOMAIN, "auth_login", ("auth_login",), ("builder", "auth", "login"), cli_public=False),
111
- PublicToolSpec(BUILDER_DOMAIN, "auth_use_token", ("auth_use_token",), ("builder", "auth", "use-token"), cli_public=False),
108
+ PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("builder", "auth", "use-credential"), cli_public=False),
112
109
  PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
113
110
  PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
114
111
  PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
115
- PublicToolSpec(BUILDER_DOMAIN, "workspace_select", ("workspace_select",), ("builder", "workspace", "select"), cli_public=False),
116
112
  PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
117
113
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
118
114
  PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
@@ -263,12 +263,6 @@ def _trim_workspace_list(payload: JSONObject) -> None:
263
263
  _trim_item_list(page, "list", allowed=("wsId", "workspaceName", "remark"))
264
264
 
265
265
 
266
- def _trim_workspace_select(payload: JSONObject) -> None:
267
- workspace = payload.get("workspace")
268
- if isinstance(workspace, dict):
269
- payload["workspace"] = _pick(workspace, ("wsId", "workspaceName"))
270
-
271
-
272
266
  def _trim_app_search_like(payload: JSONObject) -> None:
273
267
  payload.pop("apps", None)
274
268
  _trim_item_list(payload, "items", allowed=("app_key", "app_name", "package_name"))
@@ -731,10 +725,9 @@ def _register_policy(domains: tuple[str, ...], names: tuple[str, ...], transform
731
725
  SUCCESS_POLICY_BY_TOOL[tool_key(domain, name)] = transform
732
726
 
733
727
 
734
- _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_login", "auth_use_token", "auth_whoami"), _trim_auth_payload)
728
+ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_use_credential", "auth_whoami"), _trim_auth_payload)
735
729
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("auth_logout",), _trim_auth_logout)
736
730
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_workspace_list)
737
- _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_select",), _trim_workspace_select)
738
731
  _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
739
732
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
740
733
  _register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
@@ -34,7 +34,7 @@ def build_server() -> FastMCP:
34
34
 
35
35
  ## Authentication
36
36
 
37
- Use `auth_login` first, then `workspace_list` and `workspace_select`.
37
+ Use `auth_use_credential` first when a local host such as createClaw can provide a credential. Treat the returned `wsId` and `qfVersion` as authoritative for the local session.
38
38
  All resource tools operate with the logged-in user's Qingflow permissions.
39
39
 
40
40
  ## Shared Helper