@josephyan/qingflow-cli 0.2.0-beta.73 → 0.2.0-beta.74
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/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/qingflow_mcp/backend_client.py +102 -0
- package/src/qingflow_mcp/builder_facade/service.py +609 -5
- package/src/qingflow_mcp/cli/commands/builder.py +33 -1
- package/src/qingflow_mcp/cli/commands/repo.py +80 -0
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/config.py +147 -0
- package/src/qingflow_mcp/repository_store.py +71 -0
- package/src/qingflow_mcp/response_trim.py +4 -0
- package/src/qingflow_mcp/server_app_builder.py +26 -1
- package/src/qingflow_mcp/tools/ai_builder_tools.py +110 -0
- package/src/qingflow_mcp/tools/repository_dev_tools.py +533 -0
|
@@ -77,6 +77,14 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
77
77
|
package_resolve.add_argument("--package-name", required=True)
|
|
78
78
|
package_resolve.set_defaults(handler=_handle_package_resolve, format_hint="builder_summary")
|
|
79
79
|
|
|
80
|
+
solution = builder_subparsers.add_parser("solution", help="解决方案")
|
|
81
|
+
solution_subparsers = solution.add_subparsers(dest="builder_solution_command", required=True)
|
|
82
|
+
solution_install = solution_subparsers.add_parser("install", help="安装解决方案")
|
|
83
|
+
solution_install.add_argument("--solution-key", required=True)
|
|
84
|
+
solution_install.add_argument("--being-copy-data", action=argparse.BooleanOptionalAction, default=True)
|
|
85
|
+
solution_install.add_argument("--solution-source", default="solutionDetail")
|
|
86
|
+
solution_install.set_defaults(handler=_handle_solution_install, format_hint="builder_summary")
|
|
87
|
+
|
|
80
88
|
package_create = package_subparsers.add_parser("create", help="创建应用包")
|
|
81
89
|
package_create.add_argument("--package-name", required=True)
|
|
82
90
|
package_create.add_argument("--icon")
|
|
@@ -114,6 +122,12 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
114
122
|
app_get.add_argument("--app-key", required=True)
|
|
115
123
|
app_get.set_defaults(handler=_handle_app_get, format_hint="builder_summary")
|
|
116
124
|
|
|
125
|
+
app_repair_code_blocks = app_subparsers.add_parser("repair-code-blocks", help="扫描或修复现有代码块配置")
|
|
126
|
+
app_repair_code_blocks.add_argument("--app-key", required=True)
|
|
127
|
+
app_repair_code_blocks.add_argument("--field")
|
|
128
|
+
app_repair_code_blocks.add_argument("--apply", action="store_true")
|
|
129
|
+
app_repair_code_blocks.set_defaults(handler=_handle_app_repair_code_blocks, format_hint="builder_summary")
|
|
130
|
+
|
|
117
131
|
button = builder_subparsers.add_parser("button", help="自定义按钮")
|
|
118
132
|
button_subparsers = button.add_subparsers(dest="builder_button_command", required=True)
|
|
119
133
|
|
|
@@ -144,7 +158,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
144
158
|
|
|
145
159
|
portal = builder_subparsers.add_parser("portal", help="门户")
|
|
146
160
|
portal_subparsers = portal.add_subparsers(dest="builder_portal_command", required=True)
|
|
147
|
-
portal_list = portal_subparsers.add_parser("list", help="
|
|
161
|
+
portal_list = portal_subparsers.add_parser("list", help="列出可配置门户")
|
|
148
162
|
portal_list.set_defaults(handler=_handle_portal_list, format_hint="builder_summary")
|
|
149
163
|
|
|
150
164
|
portal_get = portal_subparsers.add_parser("get", help="读取门户详情")
|
|
@@ -310,6 +324,15 @@ def _handle_package_resolve(args: argparse.Namespace, context: CliContext) -> di
|
|
|
310
324
|
return context.builder.package_resolve(profile=args.profile, package_name=args.package_name)
|
|
311
325
|
|
|
312
326
|
|
|
327
|
+
def _handle_solution_install(args: argparse.Namespace, context: CliContext) -> dict:
|
|
328
|
+
return context.builder.solution_install(
|
|
329
|
+
profile=args.profile,
|
|
330
|
+
solution_key=args.solution_key,
|
|
331
|
+
being_copy_data=bool(args.being_copy_data),
|
|
332
|
+
solution_source=args.solution_source,
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
|
|
313
336
|
def _handle_package_create(args: argparse.Namespace, context: CliContext) -> dict:
|
|
314
337
|
return context.builder.package_create(
|
|
315
338
|
profile=args.profile,
|
|
@@ -404,6 +427,15 @@ def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
404
427
|
return handlers[args.builder_app_get_section](profile=args.profile, app_key=args.app_key)
|
|
405
428
|
|
|
406
429
|
|
|
430
|
+
def _handle_app_repair_code_blocks(args: argparse.Namespace, context: CliContext) -> dict:
|
|
431
|
+
return context.builder.app_repair_code_blocks(
|
|
432
|
+
profile=args.profile,
|
|
433
|
+
app_key=args.app_key,
|
|
434
|
+
field=args.field,
|
|
435
|
+
apply=bool(args.apply),
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
407
439
|
def _handle_portal_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
408
440
|
return context.builder.portal_list(profile=args.profile)
|
|
409
441
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from ..context import CliContext
|
|
6
|
+
from .common import raise_config_error, require_list_arg, require_object_arg
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
10
|
+
parser = subparsers.add_parser("repo", help="代码仓库开发工具")
|
|
11
|
+
repo_subparsers = parser.add_subparsers(dest="repo_command", required=True)
|
|
12
|
+
|
|
13
|
+
init_parser = repo_subparsers.add_parser("init", help="从模板初始化仓库")
|
|
14
|
+
init_parser.add_argument("--group-name", required=True)
|
|
15
|
+
init_parser.add_argument("--repo-template", required=True)
|
|
16
|
+
init_parser.set_defaults(handler=_handle_init, format_hint="generic")
|
|
17
|
+
|
|
18
|
+
generate_parser = repo_subparsers.add_parser("generate", help="沿用官方 generate 链路生成并提交页面代码")
|
|
19
|
+
generate_parser.add_argument("--repo-name", required=True)
|
|
20
|
+
generate_parser.add_argument("--query", required=True)
|
|
21
|
+
generate_parser.add_argument("--tag-id", type=int)
|
|
22
|
+
generate_parser.add_argument("--app-keys-file")
|
|
23
|
+
generate_parser.add_argument("--extra-info-file")
|
|
24
|
+
generate_parser.add_argument("--file-messages-file")
|
|
25
|
+
generate_parser.add_argument("--agent-id", type=int)
|
|
26
|
+
generate_parser.add_argument("--allow-create-table", action="store_true")
|
|
27
|
+
generate_parser.add_argument("--route-prefix")
|
|
28
|
+
generate_parser.add_argument("--token-name")
|
|
29
|
+
generate_parser.add_argument("--session-id")
|
|
30
|
+
generate_parser.add_argument("--round-version", type=int)
|
|
31
|
+
generate_parser.add_argument("--disable-trace-log", action="store_true")
|
|
32
|
+
generate_parser.set_defaults(handler=_handle_generate, format_hint="generic")
|
|
33
|
+
|
|
34
|
+
publish_parser = repo_subparsers.add_parser("publish-prod", help="发布 develop 到生产分支")
|
|
35
|
+
publish_parser.add_argument("--repo-name", required=True)
|
|
36
|
+
publish_parser.add_argument("--confirm", action="store_true")
|
|
37
|
+
publish_parser.set_defaults(handler=_handle_publish_prod, format_hint="generic")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _handle_init(args: argparse.Namespace, context: CliContext) -> dict:
|
|
41
|
+
return context.repo.repository_init(
|
|
42
|
+
profile=args.profile,
|
|
43
|
+
group_name=args.group_name,
|
|
44
|
+
repo_template=args.repo_template,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _handle_generate(args: argparse.Namespace, context: CliContext) -> dict:
|
|
49
|
+
app_keys = require_list_arg(args.app_keys_file, option_name="--app-keys-file") if args.app_keys_file else []
|
|
50
|
+
extra_info = require_object_arg(args.extra_info_file, option_name="--extra-info-file") if args.extra_info_file else None
|
|
51
|
+
file_messages = require_list_arg(args.file_messages_file, option_name="--file-messages-file") if args.file_messages_file else []
|
|
52
|
+
return context.repo.repository_generate(
|
|
53
|
+
profile=args.profile,
|
|
54
|
+
repo_name=args.repo_name,
|
|
55
|
+
query=args.query,
|
|
56
|
+
tag_id=args.tag_id,
|
|
57
|
+
app_keys=app_keys,
|
|
58
|
+
extra_info=extra_info,
|
|
59
|
+
file_messages=file_messages,
|
|
60
|
+
being_trace_log_enabled=not args.disable_trace_log,
|
|
61
|
+
agent_id=args.agent_id,
|
|
62
|
+
allow_create_table=args.allow_create_table,
|
|
63
|
+
route_prefix=args.route_prefix,
|
|
64
|
+
token_name=args.token_name,
|
|
65
|
+
session_id=args.session_id,
|
|
66
|
+
round_version=args.round_version,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _handle_publish_prod(args: argparse.Namespace, context: CliContext) -> dict:
|
|
71
|
+
if args.confirm is not True:
|
|
72
|
+
raise_config_error(
|
|
73
|
+
"repository_publish_prod requires explicit confirmation.",
|
|
74
|
+
fix_hint="Re-run with `--confirm` after verifying the target repo and release intent.",
|
|
75
|
+
)
|
|
76
|
+
return context.repo.repository_publish_prod(
|
|
77
|
+
profile=args.profile,
|
|
78
|
+
repo_name=args.repo_name,
|
|
79
|
+
confirm=args.confirm,
|
|
80
|
+
)
|
|
@@ -12,6 +12,7 @@ from ..tools.feedback_tools import FeedbackTools
|
|
|
12
12
|
from ..tools.file_tools import FileTools
|
|
13
13
|
from ..tools.import_tools import ImportTools
|
|
14
14
|
from ..tools.record_tools import RecordTools
|
|
15
|
+
from ..tools.repository_dev_tools import RepositoryDevTools
|
|
15
16
|
from ..tools.resource_read_tools import ResourceReadTools
|
|
16
17
|
from ..tools.task_context_tools import TaskContextTools
|
|
17
18
|
from ..tools.workspace_tools import WorkspaceTools
|
|
@@ -32,6 +33,7 @@ class CliContext:
|
|
|
32
33
|
files: FileTools
|
|
33
34
|
builder_feedback: FeedbackTools
|
|
34
35
|
builder: AiBuilderTools
|
|
36
|
+
repo: RepositoryDevTools
|
|
35
37
|
|
|
36
38
|
def close(self) -> None:
|
|
37
39
|
self.backend.close()
|
|
@@ -54,4 +56,5 @@ def build_cli_context() -> CliContext:
|
|
|
54
56
|
files=FileTools(sessions, backend),
|
|
55
57
|
builder_feedback=FeedbackTools(backend, mcp_side="App Builder MCP"),
|
|
56
58
|
builder=AiBuilderTools(sessions, backend),
|
|
59
|
+
repo=RepositoryDevTools(sessions, backend),
|
|
57
60
|
)
|
|
@@ -14,6 +14,13 @@ ATTACHMENT_QUESTION_TYPE = 13
|
|
|
14
14
|
DEFAULT_BASE_URL = "https://qingflow.com/api"
|
|
15
15
|
DEFAULT_FEEDBACK_APP_KEY = "e0d017kju002"
|
|
16
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"
|
|
17
24
|
|
|
18
25
|
|
|
19
26
|
def get_mcp_home() -> Path:
|
|
@@ -25,6 +32,10 @@ def get_profiles_path() -> Path:
|
|
|
25
32
|
return get_mcp_home() / "profiles.json"
|
|
26
33
|
|
|
27
34
|
|
|
35
|
+
def get_repository_metadata_dir() -> Path:
|
|
36
|
+
return get_mcp_home() / "repository-metadata"
|
|
37
|
+
|
|
38
|
+
|
|
28
39
|
def get_config_file_paths() -> list[Path]:
|
|
29
40
|
"""
|
|
30
41
|
获取可能的配置文件路径列表,按优先级排序:
|
|
@@ -197,6 +208,142 @@ def get_log_level() -> str:
|
|
|
197
208
|
)
|
|
198
209
|
|
|
199
210
|
|
|
211
|
+
def get_repository_default_group() -> str | None:
|
|
212
|
+
value = get_config_value(
|
|
213
|
+
"repository.default_group",
|
|
214
|
+
env_var="QINGFLOW_MCP_REPOSITORY_DEFAULT_GROUP",
|
|
215
|
+
default=None,
|
|
216
|
+
)
|
|
217
|
+
normalized = str(value or "").strip()
|
|
218
|
+
return normalized or None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def get_repository_git_remote_template() -> str:
|
|
222
|
+
value = get_config_value(
|
|
223
|
+
"repository.git_remote_template",
|
|
224
|
+
env_var="QINGFLOW_MCP_REPOSITORY_GIT_REMOTE_TEMPLATE",
|
|
225
|
+
default=DEFAULT_REPOSITORY_GIT_REMOTE_TEMPLATE,
|
|
226
|
+
)
|
|
227
|
+
normalized = str(value or "").strip()
|
|
228
|
+
return normalized or DEFAULT_REPOSITORY_GIT_REMOTE_TEMPLATE
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def get_repository_preview_address_template() -> str:
|
|
232
|
+
value = get_config_value(
|
|
233
|
+
"repository.preview_address_template",
|
|
234
|
+
env_var="QINGFLOW_MCP_REPOSITORY_PREVIEW_ADDRESS_TEMPLATE",
|
|
235
|
+
default=DEFAULT_REPOSITORY_PREVIEW_ADDRESS_TEMPLATE,
|
|
236
|
+
)
|
|
237
|
+
normalized = str(value or "").strip()
|
|
238
|
+
return normalized or DEFAULT_REPOSITORY_PREVIEW_ADDRESS_TEMPLATE
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def get_repository_develop_branch() -> str:
|
|
242
|
+
value = get_config_value(
|
|
243
|
+
"repository.develop_branch",
|
|
244
|
+
env_var="QINGFLOW_MCP_REPOSITORY_DEVELOP_BRANCH",
|
|
245
|
+
default=DEFAULT_REPOSITORY_DEVELOP_BRANCH,
|
|
246
|
+
)
|
|
247
|
+
normalized = str(value or "").strip()
|
|
248
|
+
return normalized or DEFAULT_REPOSITORY_DEVELOP_BRANCH
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def get_repository_prod_branch() -> str:
|
|
252
|
+
value = get_config_value(
|
|
253
|
+
"repository.prod_branch",
|
|
254
|
+
env_var="QINGFLOW_MCP_REPOSITORY_PROD_BRANCH",
|
|
255
|
+
default=DEFAULT_REPOSITORY_PROD_BRANCH,
|
|
256
|
+
)
|
|
257
|
+
normalized = str(value or "").strip()
|
|
258
|
+
return normalized or DEFAULT_REPOSITORY_PROD_BRANCH
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def get_repository_author_name() -> str:
|
|
262
|
+
value = get_config_value(
|
|
263
|
+
"repository.author_name",
|
|
264
|
+
env_var="QINGFLOW_MCP_REPOSITORY_AUTHOR_NAME",
|
|
265
|
+
default=DEFAULT_REPOSITORY_AUTHOR_NAME,
|
|
266
|
+
)
|
|
267
|
+
normalized = str(value or "").strip()
|
|
268
|
+
return normalized or DEFAULT_REPOSITORY_AUTHOR_NAME
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def get_repository_author_email() -> str:
|
|
272
|
+
value = get_config_value(
|
|
273
|
+
"repository.author_email",
|
|
274
|
+
env_var="QINGFLOW_MCP_REPOSITORY_AUTHOR_EMAIL",
|
|
275
|
+
default=DEFAULT_REPOSITORY_AUTHOR_EMAIL,
|
|
276
|
+
)
|
|
277
|
+
normalized = str(value or "").strip()
|
|
278
|
+
return normalized or DEFAULT_REPOSITORY_AUTHOR_EMAIL
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_repository_internal_base_url() -> str | None:
|
|
282
|
+
value = get_config_value(
|
|
283
|
+
"repository.internal_base_url",
|
|
284
|
+
env_var="QINGFLOW_MCP_REPOSITORY_INTERNAL_BASE_URL",
|
|
285
|
+
default=None,
|
|
286
|
+
)
|
|
287
|
+
if value is None:
|
|
288
|
+
return None
|
|
289
|
+
normalized = normalize_base_url(str(value).strip())
|
|
290
|
+
return normalized or None
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def get_repository_internal_share_token() -> str | None:
|
|
294
|
+
value = get_config_value(
|
|
295
|
+
"repository.internal_share_token",
|
|
296
|
+
env_var="QINGFLOW_MCP_REPOSITORY_INTERNAL_SHARE_TOKEN",
|
|
297
|
+
default=None,
|
|
298
|
+
)
|
|
299
|
+
normalized = str(value or "").strip()
|
|
300
|
+
return normalized or None
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def get_repository_internal_share_token_key() -> str:
|
|
304
|
+
value = get_config_value(
|
|
305
|
+
"repository.internal_share_token_key",
|
|
306
|
+
env_var="QINGFLOW_MCP_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY",
|
|
307
|
+
default=DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY,
|
|
308
|
+
)
|
|
309
|
+
normalized = str(value or "").strip()
|
|
310
|
+
return normalized or DEFAULT_REPOSITORY_INTERNAL_SHARE_TOKEN_KEY
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_repository_generate_default_agent_id() -> int | None:
|
|
314
|
+
value = get_config_value(
|
|
315
|
+
"repository.generate.default_agent_id",
|
|
316
|
+
env_var="QINGFLOW_MCP_REPOSITORY_GENERATE_DEFAULT_AGENT_ID",
|
|
317
|
+
default=None,
|
|
318
|
+
)
|
|
319
|
+
if value is None:
|
|
320
|
+
return None
|
|
321
|
+
try:
|
|
322
|
+
return int(str(value).strip())
|
|
323
|
+
except (TypeError, ValueError):
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def get_repository_generate_default_route_prefix() -> str | None:
|
|
328
|
+
value = get_config_value(
|
|
329
|
+
"repository.generate.default_route_prefix",
|
|
330
|
+
env_var="QINGFLOW_MCP_REPOSITORY_GENERATE_DEFAULT_ROUTE_PREFIX",
|
|
331
|
+
default=None,
|
|
332
|
+
)
|
|
333
|
+
normalized = str(value or "").strip()
|
|
334
|
+
return normalized or None
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def get_repository_generate_default_token_name() -> str | None:
|
|
338
|
+
value = get_config_value(
|
|
339
|
+
"repository.generate.default_token_name",
|
|
340
|
+
env_var="QINGFLOW_MCP_REPOSITORY_GENERATE_DEFAULT_TOKEN_NAME",
|
|
341
|
+
default=None,
|
|
342
|
+
)
|
|
343
|
+
normalized = str(value or "").strip()
|
|
344
|
+
return normalized or None
|
|
345
|
+
|
|
346
|
+
|
|
200
347
|
def normalize_base_url(base_url: str | None) -> str | None:
|
|
201
348
|
"""规范化 base URL"""
|
|
202
349
|
if base_url is None:
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .config import get_repository_metadata_dir
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _utc_now() -> str:
|
|
13
|
+
return datetime.now(timezone.utc).isoformat()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _safe_key(value: str) -> str:
|
|
17
|
+
keep: list[str] = []
|
|
18
|
+
for char in value:
|
|
19
|
+
if char.isalnum() or char in {"-", "_"}:
|
|
20
|
+
keep.append(char)
|
|
21
|
+
else:
|
|
22
|
+
keep.append("_")
|
|
23
|
+
normalized = "".join(keep).strip("_")
|
|
24
|
+
return normalized or "repository"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(slots=True)
|
|
28
|
+
class RepositoryMetadataStore:
|
|
29
|
+
base_dir: Path | None = None
|
|
30
|
+
_dir: Path = field(init=False, repr=False)
|
|
31
|
+
|
|
32
|
+
def __post_init__(self) -> None:
|
|
33
|
+
self._dir = self.base_dir or get_repository_metadata_dir()
|
|
34
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
def put(self, repo_name: str, payload: dict[str, Any]) -> dict[str, Any]:
|
|
37
|
+
now = _utc_now()
|
|
38
|
+
existing = self.get(repo_name) or {}
|
|
39
|
+
data = dict(existing)
|
|
40
|
+
data.update(payload)
|
|
41
|
+
data["repo_name"] = repo_name
|
|
42
|
+
data["created_at"] = existing.get("created_at") or now
|
|
43
|
+
data["updated_at"] = now
|
|
44
|
+
self._path(repo_name).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
def get(self, repo_name: str) -> dict[str, Any] | None:
|
|
48
|
+
path = self._path(repo_name)
|
|
49
|
+
if not path.exists():
|
|
50
|
+
return None
|
|
51
|
+
try:
|
|
52
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
53
|
+
except (OSError, json.JSONDecodeError):
|
|
54
|
+
path.unlink(missing_ok=True)
|
|
55
|
+
return None
|
|
56
|
+
return payload if isinstance(payload, dict) else None
|
|
57
|
+
|
|
58
|
+
def list(self) -> list[dict[str, Any]]:
|
|
59
|
+
entries: list[dict[str, Any]] = []
|
|
60
|
+
for path in sorted(self._dir.glob("*.json")):
|
|
61
|
+
try:
|
|
62
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
63
|
+
except (OSError, json.JSONDecodeError):
|
|
64
|
+
continue
|
|
65
|
+
if isinstance(payload, dict):
|
|
66
|
+
entries.append(payload)
|
|
67
|
+
entries.sort(key=lambda item: str(item.get("updated_at") or ""), reverse=True)
|
|
68
|
+
return entries
|
|
69
|
+
|
|
70
|
+
def _path(self, repo_name: str) -> Path:
|
|
71
|
+
return self._dir / f"{_safe_key(repo_name)}.json"
|
|
@@ -210,6 +210,7 @@ def resolve_cli_tool_name(args: Any) -> str | None:
|
|
|
210
210
|
return {
|
|
211
211
|
"resolve": _tool_key(BUILDER_DOMAIN, "app_resolve"),
|
|
212
212
|
"release-edit-lock-if-mine": _tool_key(BUILDER_DOMAIN, "app_release_edit_lock_if_mine"),
|
|
213
|
+
"repair-code-blocks": _tool_key(BUILDER_DOMAIN, "app_repair_code_blocks"),
|
|
213
214
|
}.get(app_command)
|
|
214
215
|
if builder_command == "button":
|
|
215
216
|
return {
|
|
@@ -324,6 +325,7 @@ BUILDER_SERVER_METHOD_MAP = {
|
|
|
324
325
|
"app_custom_button_delete": _tool_key(BUILDER_DOMAIN, "app_custom_button_delete"),
|
|
325
326
|
"app_get": _tool_key(BUILDER_DOMAIN, "app_get"),
|
|
326
327
|
"app_get_fields": _tool_key(BUILDER_DOMAIN, "app_get_fields"),
|
|
328
|
+
"app_repair_code_blocks": _tool_key(BUILDER_DOMAIN, "app_repair_code_blocks"),
|
|
327
329
|
"app_get_layout": _tool_key(BUILDER_DOMAIN, "app_get_layout"),
|
|
328
330
|
"app_get_views": _tool_key(BUILDER_DOMAIN, "app_get_views"),
|
|
329
331
|
"app_get_flow": _tool_key(BUILDER_DOMAIN, "app_get_flow"),
|
|
@@ -609,6 +611,7 @@ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_works
|
|
|
609
611
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_select",), _trim_workspace_select)
|
|
610
612
|
_register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
|
|
611
613
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("app_get",), _trim_app_get)
|
|
614
|
+
_register_policy((BUILDER_DOMAIN,), ("app_repair_code_blocks",), _trim_builder_list_like)
|
|
612
615
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
|
|
613
616
|
_register_policy((USER_DOMAIN,), ("file_get_upload_info",), _trim_file_upload_info)
|
|
614
617
|
_register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("file_upload_local",), _trim_file_upload_local)
|
|
@@ -693,6 +696,7 @@ _register_policy(
|
|
|
693
696
|
"app_custom_button_update",
|
|
694
697
|
"app_custom_button_delete",
|
|
695
698
|
"app_get_fields",
|
|
699
|
+
"app_repair_code_blocks",
|
|
696
700
|
"app_get_layout",
|
|
697
701
|
"app_get_views",
|
|
698
702
|
"app_get_flow",
|
|
@@ -34,10 +34,12 @@ def build_builder_server() -> FastMCP:
|
|
|
34
34
|
"`feedback_submit` is always available as a cross-cutting helper when the current capability is unsupported, awkward, or still cannot satisfy the user's need after reasonable use; it does not require Qingflow login or workspace selection, and it should be called only after explicit user confirmation. "
|
|
35
35
|
"Follow the resource path resolve -> summary read -> apply -> attach -> publish_verify. "
|
|
36
36
|
"Use builder_tool_contract when you need a machine-readable contract, aliases, allowed enums, or a minimal valid example for a public builder tool. "
|
|
37
|
+
"Use solution_install when the user explicitly wants to install a packaged solution/template by solution_key, optionally copying bundled demo data. "
|
|
37
38
|
"If creating a new package may be appropriate, ask the user to confirm package creation before calling package_create; otherwise use package_resolve/package_list and app_resolve to locate resources, "
|
|
38
|
-
"app_get/app_get_fields/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for configuration reads, "
|
|
39
|
+
"app_get/app_get_fields/app_repair_code_blocks/app_get_layout/app_get_views/app_get_flow/app_get_charts/portal_list/portal_get/view_get/chart_get for configuration reads, "
|
|
39
40
|
"member_search/role_search/role_create when workflow assignees must come from the directory or role catalog, preferring roles over explicit members unless the user explicitly names members, "
|
|
40
41
|
"then app_schema_apply/app_layout_apply/app_flow_apply/app_views_apply/app_charts_apply/portal_apply to execute normalized patches; these apply tools perform planning, normalization, and dependency checks internally where applicable. Schema/layout/views noop requests skip publish, charts are immediate-live without publish and resolve targets by chart_id first then exact unique chart name, portal updates are replace-only and publish=false only guarantees draft/base-info updates, and flow should use publish=false whenever you only want draft/precheck behavior. "
|
|
42
|
+
"For code_block fields with output bindings, always use qf_output assignment rather than const/let qf_output, and use app_repair_code_blocks when an existing form hangs because output-bound fields stay loading. "
|
|
41
43
|
"Use package_attach_app to attach apps to packages, and app_publish_verify for explicit final publish verification. "
|
|
42
44
|
"For workflow edits, keep the public builder surface on stable linear flows only: start/approve/fill/copy/webhook/end. Branch and condition nodes are intentionally disabled because the backend workflow route is not front-end stable for those node types. Declare node assignees and editable fields explicitly. "
|
|
43
45
|
"If builder writes are blocked by the current user's own edit lock, use app_release_edit_lock_if_mine with the lock owner details from the failed result. "
|
|
@@ -198,6 +200,20 @@ def build_builder_server() -> FastMCP:
|
|
|
198
200
|
) -> dict:
|
|
199
201
|
return ai_builder.package_create(profile=profile, package_name=package_name, icon=icon, color=color)
|
|
200
202
|
|
|
203
|
+
@server.tool()
|
|
204
|
+
def solution_install(
|
|
205
|
+
profile: str = DEFAULT_PROFILE,
|
|
206
|
+
solution_key: str = "",
|
|
207
|
+
being_copy_data: bool = True,
|
|
208
|
+
solution_source: str = "solutionDetail",
|
|
209
|
+
) -> dict:
|
|
210
|
+
return ai_builder.solution_install(
|
|
211
|
+
profile=profile,
|
|
212
|
+
solution_key=solution_key,
|
|
213
|
+
being_copy_data=being_copy_data,
|
|
214
|
+
solution_source=solution_source,
|
|
215
|
+
)
|
|
216
|
+
|
|
201
217
|
@server.tool()
|
|
202
218
|
def member_search(
|
|
203
219
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -318,6 +334,15 @@ def build_builder_server() -> FastMCP:
|
|
|
318
334
|
def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
319
335
|
return ai_builder.app_get_fields(profile=profile, app_key=app_key)
|
|
320
336
|
|
|
337
|
+
@server.tool()
|
|
338
|
+
def app_repair_code_blocks(
|
|
339
|
+
profile: str = DEFAULT_PROFILE,
|
|
340
|
+
app_key: str = "",
|
|
341
|
+
field: str | None = None,
|
|
342
|
+
apply: bool = False,
|
|
343
|
+
) -> dict:
|
|
344
|
+
return ai_builder.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply)
|
|
345
|
+
|
|
321
346
|
@server.tool()
|
|
322
347
|
def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
323
348
|
return ai_builder.app_get_layout(profile=profile, app_key=app_key)
|
|
@@ -90,6 +90,20 @@ class AiBuilderTools(ToolBase):
|
|
|
90
90
|
) -> JSONObject:
|
|
91
91
|
return self.package_create(profile=profile, package_name=package_name, icon=icon, color=color)
|
|
92
92
|
|
|
93
|
+
@mcp.tool()
|
|
94
|
+
def solution_install(
|
|
95
|
+
profile: str = DEFAULT_PROFILE,
|
|
96
|
+
solution_key: str = "",
|
|
97
|
+
being_copy_data: bool = True,
|
|
98
|
+
solution_source: str = "solutionDetail",
|
|
99
|
+
) -> JSONObject:
|
|
100
|
+
return self.solution_install(
|
|
101
|
+
profile=profile,
|
|
102
|
+
solution_key=solution_key,
|
|
103
|
+
being_copy_data=being_copy_data,
|
|
104
|
+
solution_source=solution_source,
|
|
105
|
+
)
|
|
106
|
+
|
|
93
107
|
@mcp.tool()
|
|
94
108
|
def member_search(
|
|
95
109
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -216,6 +230,15 @@ class AiBuilderTools(ToolBase):
|
|
|
216
230
|
def app_get_fields(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
217
231
|
return self.app_get_fields(profile=profile, app_key=app_key)
|
|
218
232
|
|
|
233
|
+
@mcp.tool()
|
|
234
|
+
def app_repair_code_blocks(
|
|
235
|
+
profile: str = DEFAULT_PROFILE,
|
|
236
|
+
app_key: str = "",
|
|
237
|
+
field: str | None = None,
|
|
238
|
+
apply: bool = False,
|
|
239
|
+
) -> JSONObject:
|
|
240
|
+
return self.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply)
|
|
241
|
+
|
|
219
242
|
@mcp.tool()
|
|
220
243
|
def app_get_layout(profile: str = DEFAULT_PROFILE, app_key: str = "") -> JSONObject:
|
|
221
244
|
return self.app_get_layout(profile=profile, app_key=app_key)
|
|
@@ -519,6 +542,31 @@ class AiBuilderTools(ToolBase):
|
|
|
519
542
|
},
|
|
520
543
|
)
|
|
521
544
|
|
|
545
|
+
def solution_install(
|
|
546
|
+
self,
|
|
547
|
+
*,
|
|
548
|
+
profile: str,
|
|
549
|
+
solution_key: str,
|
|
550
|
+
being_copy_data: bool = True,
|
|
551
|
+
solution_source: str = "solutionDetail",
|
|
552
|
+
) -> JSONObject:
|
|
553
|
+
normalized_args = {
|
|
554
|
+
"solution_key": solution_key,
|
|
555
|
+
"being_copy_data": being_copy_data,
|
|
556
|
+
"solution_source": solution_source,
|
|
557
|
+
}
|
|
558
|
+
return _safe_tool_call(
|
|
559
|
+
lambda: self._facade.solution_install(
|
|
560
|
+
profile=profile,
|
|
561
|
+
solution_key=solution_key,
|
|
562
|
+
being_copy_data=being_copy_data,
|
|
563
|
+
solution_source=solution_source,
|
|
564
|
+
),
|
|
565
|
+
error_code="SOLUTION_INSTALL_FAILED",
|
|
566
|
+
normalized_args=normalized_args,
|
|
567
|
+
suggested_next_call={"tool_name": "solution_install", "arguments": {"profile": profile, **normalized_args}},
|
|
568
|
+
)
|
|
569
|
+
|
|
522
570
|
def member_search(
|
|
523
571
|
self,
|
|
524
572
|
*,
|
|
@@ -768,6 +816,22 @@ class AiBuilderTools(ToolBase):
|
|
|
768
816
|
suggested_next_call={"tool_name": "app_get_fields", "arguments": {"profile": profile, "app_key": app_key}},
|
|
769
817
|
)
|
|
770
818
|
|
|
819
|
+
def app_repair_code_blocks(
|
|
820
|
+
self,
|
|
821
|
+
*,
|
|
822
|
+
profile: str,
|
|
823
|
+
app_key: str,
|
|
824
|
+
field: str | None = None,
|
|
825
|
+
apply: bool = False,
|
|
826
|
+
) -> JSONObject:
|
|
827
|
+
normalized_args = {"app_key": app_key, "field": field, "apply": apply}
|
|
828
|
+
return _safe_tool_call(
|
|
829
|
+
lambda: self._facade.app_repair_code_blocks(profile=profile, app_key=app_key, field=field, apply=apply),
|
|
830
|
+
error_code="APP_REPAIR_CODE_BLOCKS_FAILED",
|
|
831
|
+
normalized_args=normalized_args,
|
|
832
|
+
suggested_next_call={"tool_name": "app_repair_code_blocks", "arguments": {"profile": profile, **normalized_args}},
|
|
833
|
+
)
|
|
834
|
+
|
|
771
835
|
def app_read_layout_summary(self, *, profile: str, app_key: str) -> JSONObject:
|
|
772
836
|
normalized_args = {"app_key": app_key}
|
|
773
837
|
return _safe_tool_call(
|
|
@@ -1889,6 +1953,7 @@ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
|
|
|
1889
1953
|
mapping = {
|
|
1890
1954
|
"PACKAGE_LIST_FAILED": "package list is unavailable in the current route",
|
|
1891
1955
|
"PACKAGE_RESOLVE_FAILED": "package resolution is unavailable in the current route",
|
|
1956
|
+
"SOLUTION_INSTALL_FAILED": "solution install could not complete because the app creation route or solution source is unavailable",
|
|
1892
1957
|
"PACKAGE_ATTACH_FAILED": "package attachment could not be verified in the current route",
|
|
1893
1958
|
"APP_RESOLVE_FAILED": "app resolution is unavailable in the current route",
|
|
1894
1959
|
"CUSTOM_BUTTON_LIST_FAILED": "custom button list is unavailable in the current route",
|
|
@@ -1994,6 +2059,32 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
1994
2059
|
"color": "azure",
|
|
1995
2060
|
},
|
|
1996
2061
|
},
|
|
2062
|
+
"solution_install": {
|
|
2063
|
+
"allowed_keys": ["solution_key", "being_copy_data", "solution_source"],
|
|
2064
|
+
"aliases": {
|
|
2065
|
+
"solutionKey": "solution_key",
|
|
2066
|
+
"beingCopyData": "being_copy_data",
|
|
2067
|
+
"copy_data": "being_copy_data",
|
|
2068
|
+
"copyData": "being_copy_data",
|
|
2069
|
+
"solutionSource": "solution_source",
|
|
2070
|
+
"source": "solution_source",
|
|
2071
|
+
},
|
|
2072
|
+
"allowed_values": {
|
|
2073
|
+
"being_copy_data": [True, False],
|
|
2074
|
+
"solution_source": ["solutionDetail"],
|
|
2075
|
+
},
|
|
2076
|
+
"execution_notes": [
|
|
2077
|
+
"solution_install calls the backend app creation route with a solutionKey payload",
|
|
2078
|
+
"this is a write operation that may create multiple apps at once",
|
|
2079
|
+
"app_keys are verified from the backend response when available",
|
|
2080
|
+
],
|
|
2081
|
+
"minimal_example": {
|
|
2082
|
+
"profile": "default",
|
|
2083
|
+
"solution_key": "cfqrhth99401",
|
|
2084
|
+
"being_copy_data": True,
|
|
2085
|
+
"solution_source": "solutionDetail",
|
|
2086
|
+
},
|
|
2087
|
+
},
|
|
1997
2088
|
"member_search": {
|
|
1998
2089
|
"allowed_keys": ["query", "page_num", "page_size", "contain_disable"],
|
|
1999
2090
|
"aliases": {},
|
|
@@ -2273,6 +2364,8 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2273
2364
|
"q_linker_binding lets you declare request config, dynamic inputs, alias parsing, and target-field bindings in one step; builder writes remoteLookupConfig plus the existing backend relation-default and questionRelations structures",
|
|
2274
2365
|
"code_block_binding lets you declare inputs, code, alias parsing, and target-field bindings in one step; builder writes codeBlockConfig plus the existing backend relation-default and questionRelations structures",
|
|
2275
2366
|
"builder configures code blocks only; it does not execute or trigger code blocks",
|
|
2367
|
+
"code block outputs must be emitted through qf_output assignment; use qf_output = {...} or assign qf_output after building the result object, never const/let qf_output =",
|
|
2368
|
+
"builder automatically normalizes const/let qf_output assignments on write and rejects output-bound code blocks that still do not contain a valid qf_output assignment",
|
|
2276
2369
|
"code_block_binding and q_linker_binding target fields are limited to text, long_text, number, amount, date, datetime, single_select, multi_select, and boolean",
|
|
2277
2370
|
],
|
|
2278
2371
|
"minimal_example": {
|
|
@@ -2858,6 +2951,23 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
2858
2951
|
"expected_package_tag_id": 1001,
|
|
2859
2952
|
},
|
|
2860
2953
|
},
|
|
2954
|
+
"app_repair_code_blocks": {
|
|
2955
|
+
"allowed_keys": ["app_key", "field", "apply"],
|
|
2956
|
+
"aliases": {},
|
|
2957
|
+
"allowed_values": {},
|
|
2958
|
+
"execution_notes": [
|
|
2959
|
+
"scan existing code_block fields for qf_output shadowing, missing output assignment, and broken writeback defaults",
|
|
2960
|
+
"apply=false is a dry-run and returns a repair plan without writing builder config",
|
|
2961
|
+
"apply=true rewrites only safe cases: const/let qf_output shadowing and stale relation-default output bindings reconstructed from readable code_block_binding config",
|
|
2962
|
+
"this tool does not invent missing output aliases or guess missing target bindings when only low-level codeBlockConfig is present",
|
|
2963
|
+
],
|
|
2964
|
+
"minimal_example": {
|
|
2965
|
+
"profile": "default",
|
|
2966
|
+
"app_key": "APP_KEY",
|
|
2967
|
+
"field": "线索价值评估",
|
|
2968
|
+
"apply": False,
|
|
2969
|
+
},
|
|
2970
|
+
},
|
|
2861
2971
|
}
|
|
2862
2972
|
|
|
2863
2973
|
_PRIVATE_BUILDER_TOOL_CONTRACTS = {
|