@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.
- package/README.md +2 -2
- package/docs/local-agent-install.md +70 -11
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/builder_facade/service.py +47 -21
- package/src/qingflow_mcp/cli/commands/auth.py +14 -43
- package/src/qingflow_mcp/cli/commands/task.py +4 -1
- package/src/qingflow_mcp/cli/commands/workspace.py +0 -8
- package/src/qingflow_mcp/cli/formatters.py +0 -21
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/public_surface.py +2 -6
- package/src/qingflow_mcp/response_trim.py +1 -8
- package/src/qingflow_mcp/server.py +1 -1
- package/src/qingflow_mcp/server_app_builder.py +4 -28
- package/src/qingflow_mcp/server_app_user.py +4 -28
- package/src/qingflow_mcp/session_store.py +31 -5
- package/src/qingflow_mcp/tools/ai_builder_tools.py +117 -1
- package/src/qingflow_mcp/tools/app_tools.py +51 -1
- package/src/qingflow_mcp/tools/approval_tools.py +82 -1
- package/src/qingflow_mcp/tools/auth_tools.py +258 -288
- package/src/qingflow_mcp/tools/base.py +204 -4
- package/src/qingflow_mcp/tools/code_block_tools.py +21 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +24 -1
- package/src/qingflow_mcp/tools/directory_tools.py +28 -1
- package/src/qingflow_mcp/tools/feedback_tools.py +8 -0
- package/src/qingflow_mcp/tools/file_tools.py +25 -1
- package/src/qingflow_mcp/tools/import_tools.py +40 -1
- package/src/qingflow_mcp/tools/navigation_tools.py +34 -1
- package/src/qingflow_mcp/tools/package_tools.py +37 -1
- package/src/qingflow_mcp/tools/portal_tools.py +28 -1
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +38 -1
- package/src/qingflow_mcp/tools/record_tools.py +255 -2
- package/src/qingflow_mcp/tools/repository_dev_tools.py +21 -2
- package/src/qingflow_mcp/tools/resource_read_tools.py +23 -1
- package/src/qingflow_mcp/tools/role_tools.py +19 -1
- package/src/qingflow_mcp/tools/solution_tools.py +56 -1
- package/src/qingflow_mcp/tools/task_context_tools.py +205 -6
- package/src/qingflow_mcp/tools/task_tools.py +49 -3
- package/src/qingflow_mcp/tools/view_tools.py +56 -1
- package/src/qingflow_mcp/tools/workflow_tools.py +65 -1
- 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.
|
|
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.
|
|
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/
|
|
67
|
-
dist/npm/
|
|
68
|
-
dist/npm/
|
|
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/
|
|
75
|
-
npm install /absolute/path/to/dist/npm/
|
|
76
|
-
npm install /absolute/path/to/dist/npm/
|
|
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.
|
|
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
|
@@ -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
|
-
|
|
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
|
-
"
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
),
|
|
769
|
+
"package_name_verified": package_name_verified,
|
|
770
|
+
"package_icon_verified": package_icon_verified,
|
|
771
|
+
"visibility_verified": visibility_verified,
|
|
746
772
|
},
|
|
747
|
-
"verified":
|
|
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": "
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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, "
|
|
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, "
|
|
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), ("
|
|
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 `
|
|
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
|