@josephyan/qingflow-cli 0.2.0-beta.985 → 0.2.0-beta.987
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 +376 -19
- package/src/qingflow_mcp/cli/commands/auth.py +14 -43
- package/src/qingflow_mcp/cli/commands/workspace.py +8 -5
- package/src/qingflow_mcp/cli/formatters.py +19 -22
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/errors.py +2 -2
- package/src/qingflow_mcp/public_surface.py +4 -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/solution/compiler/form_compiler.py +2 -2
- package/src/qingflow_mcp/solution/executor.py +2 -2
- 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 +306 -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 +72 -1
- 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 +100 -217
|
@@ -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,9 +15,9 @@ 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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
get_parser = workspace_subparsers.add_parser("get", help="读取工作区详情")
|
|
19
|
+
get_parser.add_argument("--ws-id", type=int, default=0)
|
|
20
|
+
get_parser.set_defaults(handler=_handle_get, format_hint="workspace_get")
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
@@ -29,5 +29,8 @@ def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
29
29
|
)
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def
|
|
33
|
-
return context.workspace.
|
|
32
|
+
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
33
|
+
return context.workspace.workspace_get(
|
|
34
|
+
profile=args.profile,
|
|
35
|
+
ws_id=args.ws_id if int(args.ws_id or 0) > 0 else None,
|
|
36
|
+
)
|
|
@@ -38,8 +38,12 @@ def _format_whoami(result: dict[str, Any]) -> str:
|
|
|
38
38
|
f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
|
|
39
39
|
f"UID: {result.get('uid')}",
|
|
40
40
|
f"Workspace: {result.get('selected_ws_name') or '-'} ({result.get('selected_ws_id') or '-'})",
|
|
41
|
-
f"QF Version: {result.get('qf_version') or '-'}",
|
|
41
|
+
f"Workspace QF Version: {result.get('qf_version') or '-'}",
|
|
42
42
|
]
|
|
43
|
+
request_route = result.get("request_route") if isinstance(result.get("request_route"), dict) else {}
|
|
44
|
+
route_qf_version = request_route.get("qf_version")
|
|
45
|
+
if route_qf_version and route_qf_version != result.get("qf_version"):
|
|
46
|
+
lines.append(f"Request Route QF Version: {route_qf_version}")
|
|
43
47
|
lines.append(f"Permission Level: {result.get('permission_level') or '-'}")
|
|
44
48
|
departments = result.get("departments") if isinstance(result.get("departments"), list) else []
|
|
45
49
|
roles = result.get("roles") if isinstance(result.get("roles"), list) else []
|
|
@@ -82,6 +86,19 @@ def _format_workspace_list(result: dict[str, Any]) -> str:
|
|
|
82
86
|
return _render_titled_table("Workspaces", ["ws_id", "name", "remark"], rows)
|
|
83
87
|
|
|
84
88
|
|
|
89
|
+
def _format_workspace_get(result: dict[str, Any]) -> str:
|
|
90
|
+
workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
|
|
91
|
+
lines = [
|
|
92
|
+
f"Workspace: {workspace.get('workspaceName') or workspace.get('wsName') or '-'} ({workspace.get('wsId') or result.get('ws_id') or '-'})",
|
|
93
|
+
f"QF Version: {result.get('qf_version') or workspace.get('systemVersion') or '-'}",
|
|
94
|
+
f"Identity: {workspace.get('identity') or '-'}",
|
|
95
|
+
f"Auth: {workspace.get('auth') if workspace.get('auth') is not None else '-'}",
|
|
96
|
+
f"State: {workspace.get('state') if workspace.get('state') is not None else '-'}",
|
|
97
|
+
]
|
|
98
|
+
_append_warnings(lines, result.get("warnings"))
|
|
99
|
+
return "\n".join(lines) + "\n"
|
|
100
|
+
|
|
101
|
+
|
|
85
102
|
def _format_app_items(result: dict[str, Any]) -> str:
|
|
86
103
|
items = result.get("items")
|
|
87
104
|
if not isinstance(items, list):
|
|
@@ -132,26 +149,6 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
132
149
|
_append_warnings(lines, result.get("warnings"))
|
|
133
150
|
return "\n".join(lines) + "\n"
|
|
134
151
|
|
|
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
152
|
def _format_record_list(result: dict[str, Any]) -> str:
|
|
156
153
|
data = result.get("data") if isinstance(result.get("data"), dict) else {}
|
|
157
154
|
items = data.get("items") if isinstance(data.get("items"), list) else []
|
|
@@ -364,7 +361,7 @@ def _first_present(payload: dict[str, Any], *keys: str) -> Any:
|
|
|
364
361
|
_FORMATTERS = {
|
|
365
362
|
"auth_whoami": _format_whoami,
|
|
366
363
|
"workspace_list": _format_workspace_list,
|
|
367
|
-
"
|
|
364
|
+
"workspace_get": _format_workspace_get,
|
|
368
365
|
"app_list": _format_app_items,
|
|
369
366
|
"app_search": _format_app_items,
|
|
370
367
|
"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,11 @@ 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, "
|
|
37
|
+
PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
|
|
39
38
|
PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
|
|
40
39
|
PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
|
|
41
40
|
PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
|
|
@@ -107,12 +106,11 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
107
106
|
|
|
108
107
|
|
|
109
108
|
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),
|
|
109
|
+
PublicToolSpec(BUILDER_DOMAIN, "auth_use_credential", ("auth_use_credential",), ("builder", "auth", "use-credential"), cli_public=False),
|
|
112
110
|
PublicToolSpec(BUILDER_DOMAIN, "auth_whoami", ("auth_whoami",), ("builder", "auth", "whoami"), cli_public=False),
|
|
113
111
|
PublicToolSpec(BUILDER_DOMAIN, "auth_logout", ("auth_logout",), ("builder", "auth", "logout"), cli_public=False),
|
|
114
112
|
PublicToolSpec(BUILDER_DOMAIN, "workspace_list", ("workspace_list",), ("builder", "workspace", "list"), cli_public=False),
|
|
115
|
-
PublicToolSpec(BUILDER_DOMAIN, "
|
|
113
|
+
PublicToolSpec(BUILDER_DOMAIN, "workspace_get", ("workspace_get",), ("builder", "workspace", "get"), cli_public=False),
|
|
116
114
|
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
115
|
PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
|
|
118
116
|
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
|
|
@@ -56,38 +56,18 @@ def build_builder_server() -> FastMCP:
|
|
|
56
56
|
feedback = FeedbackTools(backend, mcp_side="App Builder MCP")
|
|
57
57
|
|
|
58
58
|
@server.tool()
|
|
59
|
-
def
|
|
59
|
+
def auth_use_credential(
|
|
60
60
|
profile: str = DEFAULT_PROFILE,
|
|
61
61
|
base_url: str | None = None,
|
|
62
62
|
qf_version: str | None = None,
|
|
63
|
-
|
|
64
|
-
password: str = "",
|
|
65
|
-
persist: bool = True,
|
|
66
|
-
) -> dict:
|
|
67
|
-
return auth.auth_login(
|
|
68
|
-
profile=profile,
|
|
69
|
-
base_url=base_url,
|
|
70
|
-
qf_version=qf_version,
|
|
71
|
-
email=email,
|
|
72
|
-
password=password,
|
|
73
|
-
persist=persist,
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
@server.tool()
|
|
77
|
-
def auth_use_token(
|
|
78
|
-
profile: str = DEFAULT_PROFILE,
|
|
79
|
-
base_url: str | None = None,
|
|
80
|
-
qf_version: str | None = None,
|
|
81
|
-
token: str = "",
|
|
82
|
-
ws_id: int | None = None,
|
|
63
|
+
credential: str = "",
|
|
83
64
|
persist: bool = False,
|
|
84
65
|
) -> dict:
|
|
85
|
-
return auth.
|
|
66
|
+
return auth.auth_use_credential(
|
|
86
67
|
profile=profile,
|
|
87
68
|
base_url=base_url,
|
|
88
69
|
qf_version=qf_version,
|
|
89
|
-
|
|
90
|
-
ws_id=ws_id,
|
|
70
|
+
credential=credential,
|
|
91
71
|
persist=persist,
|
|
92
72
|
)
|
|
93
73
|
|
|
@@ -113,10 +93,6 @@ def build_builder_server() -> FastMCP:
|
|
|
113
93
|
include_external=include_external,
|
|
114
94
|
)
|
|
115
95
|
|
|
116
|
-
@server.tool()
|
|
117
|
-
def workspace_select(profile: str = DEFAULT_PROFILE, ws_id: int = 0) -> dict:
|
|
118
|
-
return workspace.workspace_select(profile=profile, ws_id=ws_id)
|
|
119
|
-
|
|
120
96
|
@server.tool()
|
|
121
97
|
def file_upload_local(
|
|
122
98
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -191,38 +191,18 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
191
191
|
directory_tools = wrap_trimmed_methods(DirectoryTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
192
192
|
|
|
193
193
|
@server.tool()
|
|
194
|
-
def
|
|
194
|
+
def auth_use_credential(
|
|
195
195
|
profile: str = DEFAULT_PROFILE,
|
|
196
196
|
base_url: str | None = None,
|
|
197
197
|
qf_version: str | None = None,
|
|
198
|
-
|
|
199
|
-
password: str = "",
|
|
200
|
-
persist: bool = True,
|
|
201
|
-
) -> dict:
|
|
202
|
-
return auth.auth_login(
|
|
203
|
-
profile=profile,
|
|
204
|
-
base_url=base_url,
|
|
205
|
-
qf_version=qf_version,
|
|
206
|
-
email=email,
|
|
207
|
-
password=password,
|
|
208
|
-
persist=persist,
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
@server.tool()
|
|
212
|
-
def auth_use_token(
|
|
213
|
-
profile: str = DEFAULT_PROFILE,
|
|
214
|
-
base_url: str | None = None,
|
|
215
|
-
qf_version: str | None = None,
|
|
216
|
-
token: str = "",
|
|
217
|
-
ws_id: int | None = None,
|
|
198
|
+
credential: str = "",
|
|
218
199
|
persist: bool = False,
|
|
219
200
|
) -> dict:
|
|
220
|
-
return auth.
|
|
201
|
+
return auth.auth_use_credential(
|
|
221
202
|
profile=profile,
|
|
222
203
|
base_url=base_url,
|
|
223
204
|
qf_version=qf_version,
|
|
224
|
-
|
|
225
|
-
ws_id=ws_id,
|
|
205
|
+
credential=credential,
|
|
226
206
|
persist=persist,
|
|
227
207
|
)
|
|
228
208
|
|
|
@@ -248,10 +228,6 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
248
228
|
include_external=include_external,
|
|
249
229
|
)
|
|
250
230
|
|
|
251
|
-
@server.tool()
|
|
252
|
-
def workspace_select(profile: str = DEFAULT_PROFILE, ws_id: int = 0) -> dict:
|
|
253
|
-
return workspace.workspace_select(profile=profile, ws_id=ws_id)
|
|
254
|
-
|
|
255
231
|
@server.tool()
|
|
256
232
|
def app_list(profile: str = DEFAULT_PROFILE) -> dict:
|
|
257
233
|
return apps.app_list(profile=profile)
|
|
@@ -29,6 +29,9 @@ class SessionProfile:
|
|
|
29
29
|
base_url: str
|
|
30
30
|
qf_version: str | None
|
|
31
31
|
qf_version_source: str | None
|
|
32
|
+
token: str | None
|
|
33
|
+
login_token: str | None
|
|
34
|
+
credential: str | None
|
|
32
35
|
uid: int
|
|
33
36
|
email: str | None
|
|
34
37
|
nick_name: str | None
|
|
@@ -45,6 +48,9 @@ class SessionProfile:
|
|
|
45
48
|
base_url=value["base_url"],
|
|
46
49
|
qf_version=value.get("qf_version"),
|
|
47
50
|
qf_version_source=value.get("qf_version_source"),
|
|
51
|
+
token=value.get("token"),
|
|
52
|
+
login_token=value.get("login_token"),
|
|
53
|
+
credential=value.get("credential"),
|
|
48
54
|
uid=value["uid"],
|
|
49
55
|
email=value.get("email"),
|
|
50
56
|
nick_name=value.get("nick_name"),
|
|
@@ -60,6 +66,7 @@ class SessionProfile:
|
|
|
60
66
|
class BackendSession:
|
|
61
67
|
token: str
|
|
62
68
|
login_token: str | None
|
|
69
|
+
credential: str | None
|
|
63
70
|
profile: str
|
|
64
71
|
base_url: str
|
|
65
72
|
qf_version: str | None
|
|
@@ -85,6 +92,7 @@ class SessionStore:
|
|
|
85
92
|
qf_version_source: str | None = None,
|
|
86
93
|
token: str,
|
|
87
94
|
login_token: str | None,
|
|
95
|
+
credential: str | None = None,
|
|
88
96
|
uid: int,
|
|
89
97
|
email: str | None,
|
|
90
98
|
nick_name: str | None,
|
|
@@ -99,14 +107,22 @@ class SessionStore:
|
|
|
99
107
|
self._set_secret(self._login_token_key(profile), login_token)
|
|
100
108
|
else:
|
|
101
109
|
self._delete_secret(self._login_token_key(profile))
|
|
110
|
+
if credential:
|
|
111
|
+
self._set_secret(self._credential_key(profile), credential)
|
|
112
|
+
else:
|
|
113
|
+
self._delete_secret(self._credential_key(profile))
|
|
102
114
|
else:
|
|
103
115
|
self._delete_secret(self._token_key(profile))
|
|
104
116
|
self._delete_secret(self._login_token_key(profile))
|
|
117
|
+
self._delete_secret(self._credential_key(profile))
|
|
105
118
|
session_profile = SessionProfile(
|
|
106
119
|
profile=profile,
|
|
107
120
|
base_url=normalize_base_url(base_url) or base_url,
|
|
108
121
|
qf_version=(str(qf_version).strip() or None) if qf_version is not None else None,
|
|
109
122
|
qf_version_source=(str(qf_version_source).strip() or None) if qf_version_source is not None else None,
|
|
123
|
+
token=str(token).strip() or None,
|
|
124
|
+
login_token=(str(login_token).strip() or None) if login_token is not None else None,
|
|
125
|
+
credential=(str(credential).strip() or None) if credential is not None else None,
|
|
110
126
|
uid=uid,
|
|
111
127
|
email=email,
|
|
112
128
|
nick_name=nick_name,
|
|
@@ -117,8 +133,9 @@ class SessionStore:
|
|
|
117
133
|
updated_at=now,
|
|
118
134
|
)
|
|
119
135
|
self._memory_sessions[profile] = BackendSession(
|
|
120
|
-
token=token,
|
|
121
|
-
login_token=login_token,
|
|
136
|
+
token=session_profile.token or token,
|
|
137
|
+
login_token=session_profile.login_token,
|
|
138
|
+
credential=session_profile.credential,
|
|
122
139
|
profile=profile,
|
|
123
140
|
base_url=session_profile.base_url,
|
|
124
141
|
qf_version=session_profile.qf_version,
|
|
@@ -146,14 +163,19 @@ class SessionStore:
|
|
|
146
163
|
memory_session.qf_version = session_profile.qf_version
|
|
147
164
|
memory_session.qf_version_source = session_profile.qf_version_source
|
|
148
165
|
return memory_session
|
|
149
|
-
if not session_profile
|
|
166
|
+
if not session_profile:
|
|
150
167
|
return None
|
|
151
|
-
token = self._get_secret(self._token_key(profile))
|
|
168
|
+
token = self._get_secret(self._token_key(profile)) if session_profile.persisted else None
|
|
169
|
+
if not token:
|
|
170
|
+
token = session_profile.token
|
|
152
171
|
if not token:
|
|
153
172
|
return None
|
|
173
|
+
login_token = self._get_secret(self._login_token_key(profile)) if session_profile.persisted else None
|
|
174
|
+
credential = self._get_secret(self._credential_key(profile)) if session_profile.persisted else None
|
|
154
175
|
backend_session = BackendSession(
|
|
155
176
|
token=token,
|
|
156
|
-
login_token=
|
|
177
|
+
login_token=login_token or session_profile.login_token,
|
|
178
|
+
credential=credential or session_profile.credential,
|
|
157
179
|
profile=profile,
|
|
158
180
|
base_url=session_profile.base_url,
|
|
159
181
|
qf_version=session_profile.qf_version,
|
|
@@ -234,6 +256,7 @@ class SessionStore:
|
|
|
234
256
|
self._logged_out_profiles.discard(profile)
|
|
235
257
|
self._delete_secret(self._token_key(profile))
|
|
236
258
|
self._delete_secret(self._login_token_key(profile))
|
|
259
|
+
self._delete_secret(self._credential_key(profile))
|
|
237
260
|
payload = self._load_profiles()
|
|
238
261
|
profiles = payload.get("profiles", {})
|
|
239
262
|
if profile in profiles:
|
|
@@ -249,6 +272,9 @@ class SessionStore:
|
|
|
249
272
|
def _login_token_key(self, profile: str) -> str:
|
|
250
273
|
return f"{profile}:login-token"
|
|
251
274
|
|
|
275
|
+
def _credential_key(self, profile: str) -> str:
|
|
276
|
+
return f"{profile}:credential"
|
|
277
|
+
|
|
252
278
|
def _upsert_profile(self, profile: SessionProfile) -> None:
|
|
253
279
|
payload = self._load_profiles()
|
|
254
280
|
payload.setdefault("profiles", {})[profile.profile] = asdict(profile)
|
|
@@ -307,7 +307,7 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
|
|
|
307
307
|
"queId": que_id,
|
|
308
308
|
"queTitle": label,
|
|
309
309
|
"queType": _normalize_reference_que_type(raw_type) or "2",
|
|
310
|
-
"queAuth":
|
|
310
|
+
"queAuth": 3,
|
|
311
311
|
"ordinal": ordinal,
|
|
312
312
|
"quoteId": temp_id,
|
|
313
313
|
"_field_id": field_id,
|
|
@@ -320,7 +320,7 @@ def build_reference_config(field: dict[str, Any], temp_id: int) -> dict[str, Any
|
|
|
320
320
|
auth_ques = []
|
|
321
321
|
for ordinal, field_id in enumerate(auth_field_ids, start=1):
|
|
322
322
|
que_id = auth_field_que_ids[ordinal - 1] if ordinal - 1 < len(auth_field_que_ids) else 0
|
|
323
|
-
auth_ques.append({"queId": que_id, "queAuth":
|
|
323
|
+
auth_ques.append({"queId": que_id, "queAuth": 3, "_field_id": field_id})
|
|
324
324
|
return {
|
|
325
325
|
"referAppKey": "__TARGET_APP_KEY__",
|
|
326
326
|
"referQueId": display_field_que_id,
|
|
@@ -857,12 +857,12 @@ class SolutionExecutor:
|
|
|
857
857
|
if refer_que_id is None:
|
|
858
858
|
continue
|
|
859
859
|
resolved["queId"] = refer_que_id
|
|
860
|
-
resolved["queAuth"] = int(resolved.get("queAuth",
|
|
860
|
+
resolved["queAuth"] = int(resolved.get("queAuth", 3))
|
|
861
861
|
auth_ques.append(resolved)
|
|
862
862
|
if not auth_ques:
|
|
863
863
|
fallback_que_id = target_meta.get("by_field_id", {}).get(target_field_id)
|
|
864
864
|
if fallback_que_id is not None:
|
|
865
|
-
auth_ques.append({"queId": fallback_que_id, "queAuth":
|
|
865
|
+
auth_ques.append({"queId": fallback_que_id, "queAuth": 3})
|
|
866
866
|
reference_config["referAuthQues"] = auth_ques
|
|
867
867
|
reference_config["fieldNameShow"] = bool(reference_config.get("fieldNameShow", True))
|
|
868
868
|
fill_rules = []
|