@qingflow-tech/qingflow-app-builder-mcp 1.0.0
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 +32 -0
- package/docs/local-agent-install.md +332 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow-app-builder-mcp.mjs +7 -0
- package/npm/lib/runtime.mjs +339 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow-app-builder-mcp +15 -0
- package/skills/qingflow-app-builder/SKILL.md +251 -0
- package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder/references/create-app.md +128 -0
- package/skills/qingflow-app-builder/references/environments.md +63 -0
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +64 -0
- package/skills/qingflow-app-builder/references/solution-playbooks.md +53 -0
- package/skills/qingflow-app-builder/references/tool-selection.md +93 -0
- package/skills/qingflow-app-builder/references/update-flow.md +158 -0
- package/skills/qingflow-app-builder/references/update-layout.md +68 -0
- package/skills/qingflow-app-builder/references/update-schema.md +68 -0
- package/skills/qingflow-app-builder/references/update-views.md +162 -0
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +649 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +1836 -0
- package/src/qingflow_mcp/builder_facade/service.py +15044 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +18 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +44 -0
- package/src/qingflow_mcp/cli/commands/builder.py +538 -0
- package/src/qingflow_mcp/cli/commands/chart.py +18 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/portal.py +25 -0
- package/src/qingflow_mcp/cli/commands/record.py +331 -0
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/commands/task.py +89 -0
- package/src/qingflow_mcp/cli/commands/view.py +18 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +25 -0
- package/src/qingflow_mcp/cli/context.py +60 -0
- package/src/qingflow_mcp/cli/formatters.py +334 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +178 -0
- package/src/qingflow_mcp/config.py +513 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/public_surface.py +233 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +470 -0
- package/src/qingflow_mcp/server.py +212 -0
- package/src/qingflow_mcp/server_app_builder.py +533 -0
- package/src/qingflow_mcp/server_app_user.py +362 -0
- package/src/qingflow_mcp/session_store.py +302 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +495 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +187 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2398 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +855 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +3419 -0
- package/src/qingflow_mcp/tools/app_tools.py +925 -0
- package/src/qingflow_mcp/tools/approval_tools.py +1062 -0
- package/src/qingflow_mcp/tools/auth_tools.py +875 -0
- package/src/qingflow_mcp/tools/base.py +388 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +777 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +202 -0
- package/src/qingflow_mcp/tools/directory_tools.py +675 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +238 -0
- package/src/qingflow_mcp/tools/file_tools.py +409 -0
- package/src/qingflow_mcp/tools/import_tools.py +2189 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +210 -0
- package/src/qingflow_mcp/tools/package_tools.py +326 -0
- package/src/qingflow_mcp/tools/portal_tools.py +158 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +374 -0
- package/src/qingflow_mcp/tools/record_tools.py +14037 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +552 -0
- package/src/qingflow_mcp/tools/resource_read_tools.py +421 -0
- package/src/qingflow_mcp/tools/role_tools.py +112 -0
- package/src/qingflow_mcp/tools/solution_tools.py +4054 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +2228 -0
- package/src/qingflow_mcp/tools/task_tools.py +890 -0
- package/src/qingflow_mcp/tools/view_tools.py +335 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +376 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +125 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Any, Callable, TextIO
|
|
7
|
+
|
|
8
|
+
from ..errors import QingflowApiError
|
|
9
|
+
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
10
|
+
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
|
+
from .context import CliContext, build_cli_context
|
|
12
|
+
from .formatters import emit_json_result, emit_text_result
|
|
13
|
+
from .commands import register_all_commands
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Handler = Callable[[argparse.Namespace, CliContext], dict[str, Any]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
20
|
+
parser = argparse.ArgumentParser(prog="qingflow", description="Qingflow CLI")
|
|
21
|
+
parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
|
|
22
|
+
parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
23
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
24
|
+
register_all_commands(subparsers)
|
|
25
|
+
return parser
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(argv: list[str] | None = None) -> None:
|
|
29
|
+
raise SystemExit(run(argv))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run(
|
|
33
|
+
argv: list[str] | None = None,
|
|
34
|
+
*,
|
|
35
|
+
context_factory: Callable[[], CliContext] = build_cli_context,
|
|
36
|
+
stdout: TextIO | None = None,
|
|
37
|
+
stderr: TextIO | None = None,
|
|
38
|
+
) -> int:
|
|
39
|
+
out = stdout or sys.stdout
|
|
40
|
+
err = stderr or sys.stderr
|
|
41
|
+
parser = build_parser()
|
|
42
|
+
normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
|
|
43
|
+
try:
|
|
44
|
+
args = parser.parse_args(normalized_argv)
|
|
45
|
+
except SystemExit as exc:
|
|
46
|
+
return int(exc.code or 0)
|
|
47
|
+
handler = getattr(args, "handler", None)
|
|
48
|
+
if handler is None:
|
|
49
|
+
parser.print_help(out)
|
|
50
|
+
return 2
|
|
51
|
+
context = context_factory()
|
|
52
|
+
try:
|
|
53
|
+
if not bool(args.json):
|
|
54
|
+
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
55
|
+
result = handler(args, context)
|
|
56
|
+
except RuntimeError as exc:
|
|
57
|
+
payload = trim_error_response(_parse_error_payload(exc))
|
|
58
|
+
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
59
|
+
except QingflowApiError as exc:
|
|
60
|
+
payload = trim_error_response(exc.to_dict())
|
|
61
|
+
return _emit_error(payload, json_mode=bool(args.json), stdout=out, stderr=err)
|
|
62
|
+
finally:
|
|
63
|
+
context.close()
|
|
64
|
+
|
|
65
|
+
exit_code = _result_exit_code(result)
|
|
66
|
+
trimmed_result = trim_public_response(resolve_cli_tool_name(args), result) if isinstance(result, dict) else result
|
|
67
|
+
stream = out if bool(args.json) or exit_code == 0 else err
|
|
68
|
+
if bool(args.json):
|
|
69
|
+
emit_json_result(trimmed_result, stream=stream)
|
|
70
|
+
else:
|
|
71
|
+
emit_text_result(trimmed_result, hint=getattr(args, "format_hint", ""), stream=stream)
|
|
72
|
+
return exit_code
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
76
|
+
global_args: list[str] = []
|
|
77
|
+
remaining: list[str] = []
|
|
78
|
+
index = 0
|
|
79
|
+
while index < len(argv):
|
|
80
|
+
token = argv[index]
|
|
81
|
+
if token == "--json":
|
|
82
|
+
global_args.append(token)
|
|
83
|
+
index += 1
|
|
84
|
+
continue
|
|
85
|
+
if token == "--profile":
|
|
86
|
+
global_args.append(token)
|
|
87
|
+
if index + 1 >= len(argv):
|
|
88
|
+
global_args.append("")
|
|
89
|
+
break
|
|
90
|
+
global_args.append(argv[index + 1])
|
|
91
|
+
index += 2
|
|
92
|
+
continue
|
|
93
|
+
if token.startswith("--profile="):
|
|
94
|
+
global_args.append(token)
|
|
95
|
+
index += 1
|
|
96
|
+
continue
|
|
97
|
+
remaining.append(token)
|
|
98
|
+
index += 1
|
|
99
|
+
return global_args + remaining
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parse_error_payload(exc: RuntimeError) -> dict[str, Any]:
|
|
103
|
+
raw = str(exc)
|
|
104
|
+
try:
|
|
105
|
+
payload = json.loads(raw)
|
|
106
|
+
except json.JSONDecodeError:
|
|
107
|
+
return {"category": "runtime", "message": raw}
|
|
108
|
+
return payload if isinstance(payload, dict) else {"category": "runtime", "message": raw}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, stderr: TextIO) -> int:
|
|
112
|
+
exit_code = _error_exit_code(payload)
|
|
113
|
+
if json_mode:
|
|
114
|
+
emit_json_result(payload, stream=stdout)
|
|
115
|
+
return exit_code
|
|
116
|
+
lines = [
|
|
117
|
+
f"Category: {payload.get('category') or 'error'}",
|
|
118
|
+
f"Message: {payload.get('message') or 'Unknown error'}",
|
|
119
|
+
]
|
|
120
|
+
if payload.get("backend_code") is not None:
|
|
121
|
+
lines.append(f"Backend Code: {payload.get('backend_code')}")
|
|
122
|
+
if payload.get("request_id"):
|
|
123
|
+
lines.append(f"Request ID: {payload.get('request_id')}")
|
|
124
|
+
details = payload.get("details")
|
|
125
|
+
if isinstance(details, dict):
|
|
126
|
+
for key, value in details.items():
|
|
127
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
128
|
+
lines.append(f"{key}: {value}")
|
|
129
|
+
stderr.write("\n".join(lines) + "\n")
|
|
130
|
+
return exit_code
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _error_exit_code(payload: dict[str, Any]) -> int:
|
|
134
|
+
category = str(payload.get("category") or "").lower()
|
|
135
|
+
if category in {"auth", "workspace"}:
|
|
136
|
+
return 3
|
|
137
|
+
return 4
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _result_exit_code(result: dict[str, Any]) -> int:
|
|
141
|
+
if not isinstance(result, dict):
|
|
142
|
+
return 0
|
|
143
|
+
if result.get("ok") is False:
|
|
144
|
+
return 4
|
|
145
|
+
status = str(result.get("status") or "").lower()
|
|
146
|
+
if status in {"failed", "blocked"}:
|
|
147
|
+
return 4
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliContext, *, stream: TextIO) -> None:
|
|
152
|
+
spec = cli_public_tool_spec_from_namespace(args)
|
|
153
|
+
if spec is None or not spec.cli_show_effective_context:
|
|
154
|
+
return
|
|
155
|
+
sessions = getattr(context, "sessions", None)
|
|
156
|
+
if sessions is None or not hasattr(sessions, "get_profile"):
|
|
157
|
+
return
|
|
158
|
+
profile_name = str(getattr(args, "profile", "default") or "default")
|
|
159
|
+
try:
|
|
160
|
+
session_profile = sessions.get_profile(profile_name)
|
|
161
|
+
except Exception:
|
|
162
|
+
session_profile = None
|
|
163
|
+
workspace_id = getattr(session_profile, "selected_ws_id", None) if session_profile is not None else None
|
|
164
|
+
workspace_name = getattr(session_profile, "selected_ws_name", None) if session_profile is not None else None
|
|
165
|
+
if workspace_id is None:
|
|
166
|
+
workspace_label = "(not selected)"
|
|
167
|
+
elif workspace_name:
|
|
168
|
+
workspace_label = f"{workspace_name} ({workspace_id})"
|
|
169
|
+
else:
|
|
170
|
+
workspace_label = str(workspace_id)
|
|
171
|
+
lines = [f"Context: profile={profile_name} workspace={workspace_label}"]
|
|
172
|
+
if spec.cli_context_write and profile_name == "default":
|
|
173
|
+
lines.append("Warning: using default profile for a workspace-sensitive write command")
|
|
174
|
+
stream.write("\n".join(lines) + "\n")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
if __name__ == "__main__":
|
|
178
|
+
main()
|
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
8
|
+
|
|
9
|
+
DEFAULT_PROFILE = "default"
|
|
10
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
11
|
+
DEFAULT_USER_AGENT = "qingflow-mcp/1.0"
|
|
12
|
+
DEFAULT_RECORD_LIST_TYPE = 8
|
|
13
|
+
ATTACHMENT_QUESTION_TYPE = 13
|
|
14
|
+
DEFAULT_BASE_URL = "https://qingflow.com/api"
|
|
15
|
+
DEFAULT_FEEDBACK_APP_KEY = "e0d017kju002"
|
|
16
|
+
DEFAULT_FEEDBACK_QSOURCE_TOKEN = "mcp-feedback-7755d14748fc"
|
|
17
|
+
DEFAULT_REPOSITORY_GIT_REMOTE_TEMPLATE = "git@hackers.oalite.com:{group}/{repo}.git"
|
|
18
|
+
DEFAULT_REPOSITORY_PREVIEW_ADDRESS_TEMPLATE = "https://%s.preview.oalite.com"
|
|
19
|
+
DEFAULT_REPOSITORY_DEVELOP_BRANCH = "develop"
|
|
20
|
+
DEFAULT_REPOSITORY_PROD_BRANCH = "prod"
|
|
21
|
+
DEFAULT_REPOSITORY_AUTHOR_NAME = "qingflow-mcp"
|
|
22
|
+
DEFAULT_REPOSITORY_AUTHOR_EMAIL = "qingflow-mcp@local.invalid"
|
|
23
|
+
DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY = "tokenKey"
|
|
24
|
+
DEFAULT_CREDIT_BALANCE_PATH = "/ultron/internal/credit/balance"
|
|
25
|
+
DEFAULT_CREDIT_USAGE_RECORD_PATH = "/share/workspace/credit/usage/record"
|
|
26
|
+
DEFAULT_CREDIT_WINGS_TOKEN_HEADER_KEY = "wingsTokenKey"
|
|
27
|
+
DEFAULT_CREDIT_APAAS_TOKEN_HEADER_KEY = "serviceToken"
|
|
28
|
+
DEFAULT_CREDIT_WS_ID_HEADER_KEY = "wsId"
|
|
29
|
+
DEFAULT_CREDIT_USAGE_AMOUNT = "1"
|
|
30
|
+
DEFAULT_CREDIT_APAAS_TOKEN_VALUE = "729ed3cc-8eea-11ec-b585-52540009137b"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_mcp_home() -> Path:
|
|
34
|
+
custom_home = os.getenv("QINGFLOW_MCP_HOME")
|
|
35
|
+
return Path(custom_home).expanduser() if custom_home else Path.home() / ".qingflow-mcp"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_profiles_path() -> Path:
|
|
39
|
+
return get_mcp_home() / "profiles.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_repository_metadata_dir() -> Path:
|
|
43
|
+
return get_mcp_home() / "repository-metadata"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_config_file_paths() -> list[Path]:
|
|
47
|
+
"""
|
|
48
|
+
获取可能的配置文件路径列表,按优先级排序:
|
|
49
|
+
1. 环境变量 QINGFLOW_MCP_CONFIG_PATH 指定的路径
|
|
50
|
+
2. 当前工作目录下的 qingflow-mcp.config.json
|
|
51
|
+
3. MCP home 目录下的 config.json
|
|
52
|
+
4. 系统级配置 (Linux/Mac: /etc/qingflow-mcp/config.json)
|
|
53
|
+
"""
|
|
54
|
+
paths: list[Path] = []
|
|
55
|
+
|
|
56
|
+
# 1. 环境变量
|
|
57
|
+
env_config = os.getenv("QINGFLOW_MCP_CONFIG_PATH")
|
|
58
|
+
if env_config:
|
|
59
|
+
paths.append(Path(env_config).expanduser())
|
|
60
|
+
|
|
61
|
+
# 2. 当前工作目录
|
|
62
|
+
paths.append(Path.cwd() / "qingflow-mcp.config.json")
|
|
63
|
+
|
|
64
|
+
# 3. MCP home 目录
|
|
65
|
+
paths.append(get_mcp_home() / "config.json")
|
|
66
|
+
|
|
67
|
+
# 4. 系统级配置 (仅非 Windows)
|
|
68
|
+
if os.name != "nt":
|
|
69
|
+
paths.append(Path("/etc/qingflow-mcp/config.json"))
|
|
70
|
+
|
|
71
|
+
return paths
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def load_config_file() -> dict[str, Any]:
|
|
75
|
+
"""
|
|
76
|
+
加载第一个存在的配置文件
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
配置字典,如果没有找到配置文件则返回空字典
|
|
80
|
+
"""
|
|
81
|
+
for path in get_config_file_paths():
|
|
82
|
+
if path.exists():
|
|
83
|
+
try:
|
|
84
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
85
|
+
content = f.read()
|
|
86
|
+
# 移除 JSON 注释 (简单的行注释处理)
|
|
87
|
+
lines = []
|
|
88
|
+
for line in content.split("\n"):
|
|
89
|
+
stripped = line.strip()
|
|
90
|
+
if not stripped.startswith("//") and not stripped.startswith("#"):
|
|
91
|
+
lines.append(line)
|
|
92
|
+
return json.loads("\n".join(lines))
|
|
93
|
+
except (json.JSONDecodeError, IOError) as e:
|
|
94
|
+
# 配置文件存在但读取失败,记录警告但不中断
|
|
95
|
+
print(f"Warning: Failed to load config from {path}: {e}")
|
|
96
|
+
continue
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def get_config_value(key: str, env_var: str | None = None, default: Any = None) -> Any:
|
|
101
|
+
"""
|
|
102
|
+
获取配置值,优先级:环境变量 > 配置文件 > 默认值
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
key: 配置文件中的键名 (支持点号分隔的嵌套键,如 "profiles.default.name")
|
|
106
|
+
env_var: 环境变量名
|
|
107
|
+
default: 默认值
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
配置值
|
|
111
|
+
"""
|
|
112
|
+
# 1. 环境变量
|
|
113
|
+
if env_var:
|
|
114
|
+
env_value = os.getenv(env_var)
|
|
115
|
+
if env_value is not None:
|
|
116
|
+
return env_value
|
|
117
|
+
|
|
118
|
+
# 2. 配置文件
|
|
119
|
+
config = load_config_file()
|
|
120
|
+
keys = key.split(".")
|
|
121
|
+
value = config
|
|
122
|
+
for k in keys:
|
|
123
|
+
if isinstance(value, dict) and k in value:
|
|
124
|
+
value = value[k]
|
|
125
|
+
else:
|
|
126
|
+
value = None
|
|
127
|
+
break
|
|
128
|
+
|
|
129
|
+
if value is not None:
|
|
130
|
+
return value
|
|
131
|
+
|
|
132
|
+
# 3. 默认值
|
|
133
|
+
return default
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_default_base_url() -> str | None:
|
|
137
|
+
"""获取默认的 Qingflow 后端地址"""
|
|
138
|
+
value = get_config_value(
|
|
139
|
+
"default_base_url",
|
|
140
|
+
env_var="QINGFLOW_MCP_DEFAULT_BASE_URL",
|
|
141
|
+
default=DEFAULT_BASE_URL
|
|
142
|
+
)
|
|
143
|
+
return normalize_base_url(value) if value else None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_default_qf_version() -> str | None:
|
|
147
|
+
"""获取默认的 qfVersion 路由值"""
|
|
148
|
+
value = get_config_value(
|
|
149
|
+
"default_qf_version",
|
|
150
|
+
env_var="QINGFLOW_MCP_DEFAULT_QF_VERSION",
|
|
151
|
+
default=None,
|
|
152
|
+
)
|
|
153
|
+
if value is None:
|
|
154
|
+
return None
|
|
155
|
+
normalized = str(value).strip()
|
|
156
|
+
return normalized or None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def get_feedback_qsource_token() -> str | None:
|
|
160
|
+
"""获取反馈 q-source 被动入口 token"""
|
|
161
|
+
value = get_config_value(
|
|
162
|
+
"feedback.qsource_token",
|
|
163
|
+
env_var="QINGFLOW_MCP_FEEDBACK_QSOURCE_TOKEN",
|
|
164
|
+
default=DEFAULT_FEEDBACK_QSOURCE_TOKEN,
|
|
165
|
+
)
|
|
166
|
+
if value is None:
|
|
167
|
+
return None
|
|
168
|
+
normalized = str(value).strip()
|
|
169
|
+
return normalized or None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def get_feedback_base_url() -> str | None:
|
|
173
|
+
"""获取反馈 q-source 使用的 base URL"""
|
|
174
|
+
value = get_config_value(
|
|
175
|
+
"feedback.base_url",
|
|
176
|
+
env_var="QINGFLOW_MCP_FEEDBACK_BASE_URL",
|
|
177
|
+
default=None,
|
|
178
|
+
)
|
|
179
|
+
if value is None:
|
|
180
|
+
return get_default_base_url()
|
|
181
|
+
normalized = normalize_base_url(value)
|
|
182
|
+
return normalized or get_default_base_url()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def get_feedback_app_key() -> str:
|
|
186
|
+
"""获取内部反馈表 app_key"""
|
|
187
|
+
value = get_config_value(
|
|
188
|
+
"feedback.app_key",
|
|
189
|
+
env_var="QINGFLOW_MCP_FEEDBACK_APP_KEY",
|
|
190
|
+
default=DEFAULT_FEEDBACK_APP_KEY,
|
|
191
|
+
)
|
|
192
|
+
normalized = str(value or "").strip()
|
|
193
|
+
return normalized or DEFAULT_FEEDBACK_APP_KEY
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def get_timeout_seconds() -> float:
|
|
197
|
+
"""获取 HTTP 超时秒数"""
|
|
198
|
+
value = get_config_value(
|
|
199
|
+
"timeout_seconds",
|
|
200
|
+
env_var="QINGFLOW_MCP_TIMEOUT_SECONDS",
|
|
201
|
+
default=DEFAULT_TIMEOUT_SECONDS
|
|
202
|
+
)
|
|
203
|
+
try:
|
|
204
|
+
return float(value)
|
|
205
|
+
except (ValueError, TypeError):
|
|
206
|
+
return DEFAULT_TIMEOUT_SECONDS
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def get_log_level() -> str:
|
|
210
|
+
"""获取日志级别"""
|
|
211
|
+
return get_config_value(
|
|
212
|
+
"log_level",
|
|
213
|
+
env_var="QINGFLOW_MCP_LOG_LEVEL",
|
|
214
|
+
default="INFO"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def get_credit_meter_enabled() -> bool:
|
|
219
|
+
value = get_config_value(
|
|
220
|
+
"credit_meter.enabled",
|
|
221
|
+
env_var="QINGFLOW_MCP_CREDIT_METER_ENABLED",
|
|
222
|
+
default="true",
|
|
223
|
+
)
|
|
224
|
+
normalized = str(value or "").strip().lower()
|
|
225
|
+
return normalized in {"1", "true", "yes", "on"}
|
|
226
|
+
|
|
227
|
+
|
|
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
|
+
def get_credit_usage_base_url() -> str | None:
|
|
303
|
+
value = get_config_value(
|
|
304
|
+
"credit_meter.apaas.base_url",
|
|
305
|
+
env_var="QINGFLOW_MCP_CREDIT_APAAS_BASE_URL",
|
|
306
|
+
default=None,
|
|
307
|
+
)
|
|
308
|
+
normalized = normalize_base_url(value)
|
|
309
|
+
return normalized or None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def get_credit_usage_path() -> str:
|
|
313
|
+
value = get_config_value(
|
|
314
|
+
"credit_meter.apaas.path",
|
|
315
|
+
env_var="QINGFLOW_MCP_CREDIT_APAAS_PATH",
|
|
316
|
+
default=DEFAULT_CREDIT_USAGE_RECORD_PATH,
|
|
317
|
+
)
|
|
318
|
+
normalized = str(value or "").strip()
|
|
319
|
+
return normalized or DEFAULT_CREDIT_USAGE_RECORD_PATH
|
|
320
|
+
|
|
321
|
+
|
|
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
|
+
def get_repository_default_group() -> str | None:
|
|
357
|
+
value = get_config_value(
|
|
358
|
+
"repository.default_group",
|
|
359
|
+
env_var="QINGFLOW_MCP_REPOSITORY_DEFAULT_GROUP",
|
|
360
|
+
default=None,
|
|
361
|
+
)
|
|
362
|
+
normalized = str(value or "").strip()
|
|
363
|
+
return normalized or None
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def get_repository_git_remote_template() -> str:
|
|
367
|
+
value = get_config_value(
|
|
368
|
+
"repository.git_remote_template",
|
|
369
|
+
env_var="QINGFLOW_MCP_REPOSITORY_GIT_REMOTE_TEMPLATE",
|
|
370
|
+
default=DEFAULT_REPOSITORY_GIT_REMOTE_TEMPLATE,
|
|
371
|
+
)
|
|
372
|
+
normalized = str(value or "").strip()
|
|
373
|
+
return normalized or DEFAULT_REPOSITORY_GIT_REMOTE_TEMPLATE
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def get_repository_preview_address_template() -> str:
|
|
377
|
+
value = get_config_value(
|
|
378
|
+
"repository.preview_address_template",
|
|
379
|
+
env_var="QINGFLOW_MCP_REPOSITORY_PREVIEW_ADDRESS_TEMPLATE",
|
|
380
|
+
default=DEFAULT_REPOSITORY_PREVIEW_ADDRESS_TEMPLATE,
|
|
381
|
+
)
|
|
382
|
+
normalized = str(value or "").strip()
|
|
383
|
+
return normalized or DEFAULT_REPOSITORY_PREVIEW_ADDRESS_TEMPLATE
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def get_repository_develop_branch() -> str:
|
|
387
|
+
value = get_config_value(
|
|
388
|
+
"repository.develop_branch",
|
|
389
|
+
env_var="QINGFLOW_MCP_REPOSITORY_DEVELOP_BRANCH",
|
|
390
|
+
default=DEFAULT_REPOSITORY_DEVELOP_BRANCH,
|
|
391
|
+
)
|
|
392
|
+
normalized = str(value or "").strip()
|
|
393
|
+
return normalized or DEFAULT_REPOSITORY_DEVELOP_BRANCH
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def get_repository_prod_branch() -> str:
|
|
397
|
+
value = get_config_value(
|
|
398
|
+
"repository.prod_branch",
|
|
399
|
+
env_var="QINGFLOW_MCP_REPOSITORY_PROD_BRANCH",
|
|
400
|
+
default=DEFAULT_REPOSITORY_PROD_BRANCH,
|
|
401
|
+
)
|
|
402
|
+
normalized = str(value or "").strip()
|
|
403
|
+
return normalized or DEFAULT_REPOSITORY_PROD_BRANCH
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def get_repository_author_name() -> str:
|
|
407
|
+
value = get_config_value(
|
|
408
|
+
"repository.author_name",
|
|
409
|
+
env_var="QINGFLOW_MCP_REPOSITORY_AUTHOR_NAME",
|
|
410
|
+
default=DEFAULT_REPOSITORY_AUTHOR_NAME,
|
|
411
|
+
)
|
|
412
|
+
normalized = str(value or "").strip()
|
|
413
|
+
return normalized or DEFAULT_REPOSITORY_AUTHOR_NAME
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def get_repository_author_email() -> str:
|
|
417
|
+
value = get_config_value(
|
|
418
|
+
"repository.author_email",
|
|
419
|
+
env_var="QINGFLOW_MCP_REPOSITORY_AUTHOR_EMAIL",
|
|
420
|
+
default=DEFAULT_REPOSITORY_AUTHOR_EMAIL,
|
|
421
|
+
)
|
|
422
|
+
normalized = str(value or "").strip()
|
|
423
|
+
return normalized or DEFAULT_REPOSITORY_AUTHOR_EMAIL
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def get_repository_internal_base_url() -> str | None:
|
|
427
|
+
value = get_config_value(
|
|
428
|
+
"repository.internal_base_url",
|
|
429
|
+
env_var="QINGFLOW_MCP_REPOSITORY_INTERNAL_BASE_URL",
|
|
430
|
+
default=None,
|
|
431
|
+
)
|
|
432
|
+
if value is None:
|
|
433
|
+
return None
|
|
434
|
+
normalized = normalize_base_url(str(value).strip())
|
|
435
|
+
return normalized or None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def get_repository_internal_share_token() -> str | None:
|
|
439
|
+
value = get_config_value(
|
|
440
|
+
"repository.internal_share_token",
|
|
441
|
+
env_var="QINGFLOW_MCP_REPOSITORY_INTERNAL_SHARE_TOKEN",
|
|
442
|
+
default=None,
|
|
443
|
+
)
|
|
444
|
+
normalized = str(value or "").strip()
|
|
445
|
+
return normalized or None
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def get_repository_internal_share_token_key() -> str:
|
|
449
|
+
value = get_config_value(
|
|
450
|
+
"repository.internal_share_token_key",
|
|
451
|
+
env_var="QINGFLOW_MCP_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY",
|
|
452
|
+
default=DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY,
|
|
453
|
+
)
|
|
454
|
+
normalized = str(value or "").strip()
|
|
455
|
+
return normalized or DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def get_repository_generate_default_agent_id() -> int | None:
|
|
459
|
+
value = get_config_value(
|
|
460
|
+
"repository.generate.default_agent_id",
|
|
461
|
+
env_var="QINGFLOW_MCP_REPOSITORY_GENERATE_DEFAULT_AGENT_ID",
|
|
462
|
+
default=None,
|
|
463
|
+
)
|
|
464
|
+
if value is None:
|
|
465
|
+
return None
|
|
466
|
+
try:
|
|
467
|
+
return int(str(value).strip())
|
|
468
|
+
except (TypeError, ValueError):
|
|
469
|
+
return None
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def get_repository_generate_default_route_prefix() -> str | None:
|
|
473
|
+
value = get_config_value(
|
|
474
|
+
"repository.generate.default_route_prefix",
|
|
475
|
+
env_var="QINGFLOW_MCP_REPOSITORY_GENERATE_DEFAULT_ROUTE_PREFIX",
|
|
476
|
+
default=None,
|
|
477
|
+
)
|
|
478
|
+
normalized = str(value or "").strip()
|
|
479
|
+
return normalized or None
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def get_repository_generate_default_token_name() -> str | None:
|
|
483
|
+
value = get_config_value(
|
|
484
|
+
"repository.generate.default_token_name",
|
|
485
|
+
env_var="QINGFLOW_MCP_REPOSITORY_GENERATE_DEFAULT_TOKEN_NAME",
|
|
486
|
+
default=None,
|
|
487
|
+
)
|
|
488
|
+
normalized = str(value or "").strip()
|
|
489
|
+
return normalized or None
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def normalize_base_url(base_url: str | None) -> str | None:
|
|
493
|
+
"""规范化 base URL"""
|
|
494
|
+
if base_url is None:
|
|
495
|
+
return None
|
|
496
|
+
normalized = base_url.strip()
|
|
497
|
+
if not normalized:
|
|
498
|
+
return None
|
|
499
|
+
normalized = normalized.rstrip("/")
|
|
500
|
+
try:
|
|
501
|
+
parsed = urlsplit(normalized)
|
|
502
|
+
except ValueError:
|
|
503
|
+
return normalized
|
|
504
|
+
if not parsed.scheme or not parsed.netloc:
|
|
505
|
+
return normalized
|
|
506
|
+
|
|
507
|
+
hostname = parsed.hostname or ""
|
|
508
|
+
if hostname.lower() == "www.qingflow.com":
|
|
509
|
+
netloc = "qingflow.com"
|
|
510
|
+
if parsed.port is not None:
|
|
511
|
+
netloc = f"{netloc}:{parsed.port}"
|
|
512
|
+
normalized = urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))
|
|
513
|
+
return normalized
|