@josephyan/qingflow-cli 0.2.0-beta.72 → 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.
@@ -10,6 +10,34 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
10
10
  parser = subparsers.add_parser("builder", aliases=["build"], help="稳定 builder 命令")
11
11
  builder_subparsers = parser.add_subparsers(dest="builder_command", required=True)
12
12
 
13
+ file_parser = builder_subparsers.add_parser("file", help="builder 侧文件上传")
14
+ file_subparsers = file_parser.add_subparsers(dest="builder_file_command", required=True)
15
+ file_upload_local = file_subparsers.add_parser("upload-local", help="上传本地文件并返回附件值")
16
+ file_upload_local.add_argument("--upload-kind", default="attachment")
17
+ file_upload_local.add_argument("--file-path", required=True)
18
+ file_upload_local.add_argument("--upload-mark")
19
+ file_upload_local.add_argument("--content-type")
20
+ file_upload_local.add_argument("--bucket-type")
21
+ file_upload_local.add_argument("--path-id", type=int)
22
+ file_upload_local.add_argument("--file-related-url")
23
+ file_upload_local.set_defaults(handler=_handle_file_upload_local, format_hint="generic")
24
+
25
+ feedback = builder_subparsers.add_parser("feedback", help="builder 侧反馈提交")
26
+ feedback_subparsers = feedback.add_subparsers(dest="builder_feedback_command", required=True)
27
+ feedback_submit = feedback_subparsers.add_parser("submit", help="提交 builder 能力反馈")
28
+ feedback_submit.add_argument("--category", required=True)
29
+ feedback_submit.add_argument("--title", required=True)
30
+ feedback_submit.add_argument("--description", required=True)
31
+ feedback_submit.add_argument("--expected-behavior")
32
+ feedback_submit.add_argument("--actual-behavior")
33
+ feedback_submit.add_argument("--impact-scope")
34
+ feedback_submit.add_argument("--tool-name")
35
+ feedback_submit.add_argument("--app-key")
36
+ feedback_submit.add_argument("--record-id")
37
+ feedback_submit.add_argument("--workflow-node-id")
38
+ feedback_submit.add_argument("--note")
39
+ feedback_submit.set_defaults(handler=_handle_feedback_submit, format_hint="generic")
40
+
13
41
  contract = builder_subparsers.add_parser("contract", help="读取 builder tool 合约")
14
42
  contract.add_argument("--tool-name", required=True)
15
43
  contract.set_defaults(handler=_handle_contract, format_hint="builder_summary")
@@ -49,6 +77,14 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
49
77
  package_resolve.add_argument("--package-name", required=True)
50
78
  package_resolve.set_defaults(handler=_handle_package_resolve, format_hint="builder_summary")
51
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
+
52
88
  package_create = package_subparsers.add_parser("create", help="创建应用包")
53
89
  package_create.add_argument("--package-name", required=True)
54
90
  package_create.add_argument("--icon")
@@ -86,6 +122,12 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
86
122
  app_get.add_argument("--app-key", required=True)
87
123
  app_get.set_defaults(handler=_handle_app_get, format_hint="builder_summary")
88
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
+
89
131
  button = builder_subparsers.add_parser("button", help="自定义按钮")
90
132
  button_subparsers = button.add_subparsers(dest="builder_button_command", required=True)
91
133
 
@@ -116,7 +158,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
116
158
 
117
159
  portal = builder_subparsers.add_parser("portal", help="门户")
118
160
  portal_subparsers = portal.add_subparsers(dest="builder_portal_command", required=True)
119
- portal_list = portal_subparsers.add_parser("list", help="列出可访问门户")
161
+ portal_list = portal_subparsers.add_parser("list", help="列出可配置门户")
120
162
  portal_list.set_defaults(handler=_handle_portal_list, format_hint="builder_summary")
121
163
 
122
164
  portal_get = portal_subparsers.add_parser("get", help="读取门户详情")
@@ -215,6 +257,35 @@ def _handle_package_list(args: argparse.Namespace, context: CliContext) -> dict:
215
257
  return context.builder.package_list(profile=args.profile, trial_status=args.trial_status)
216
258
 
217
259
 
260
+ def _handle_file_upload_local(args: argparse.Namespace, context: CliContext) -> dict:
261
+ return context.files.file_upload_local(
262
+ profile=args.profile,
263
+ upload_kind=args.upload_kind,
264
+ file_path=args.file_path,
265
+ upload_mark=args.upload_mark,
266
+ content_type=args.content_type,
267
+ bucket_type=args.bucket_type,
268
+ path_id=args.path_id,
269
+ file_related_url=args.file_related_url,
270
+ )
271
+
272
+
273
+ def _handle_feedback_submit(args: argparse.Namespace, context: CliContext) -> dict:
274
+ return context.builder_feedback.feedback_submit(
275
+ category=args.category,
276
+ title=args.title,
277
+ description=args.description,
278
+ expected_behavior=args.expected_behavior,
279
+ actual_behavior=args.actual_behavior,
280
+ impact_scope=args.impact_scope,
281
+ tool_name=args.tool_name,
282
+ app_key=args.app_key,
283
+ record_id=args.record_id,
284
+ workflow_node_id=args.workflow_node_id,
285
+ note=args.note,
286
+ )
287
+
288
+
218
289
  def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
219
290
  return context.builder.builder_tool_contract(tool_name=args.tool_name)
220
291
 
@@ -253,6 +324,15 @@ def _handle_package_resolve(args: argparse.Namespace, context: CliContext) -> di
253
324
  return context.builder.package_resolve(profile=args.profile, package_name=args.package_name)
254
325
 
255
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
+
256
336
  def _handle_package_create(args: argparse.Namespace, context: CliContext) -> dict:
257
337
  return context.builder.package_create(
258
338
  profile=args.profile,
@@ -347,6 +427,15 @@ def _handle_app_get(args: argparse.Namespace, context: CliContext) -> dict:
347
427
  return handlers[args.builder_app_get_section](profile=args.profile, app_key=args.app_key)
348
428
 
349
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
+
350
439
  def _handle_portal_list(args: argparse.Namespace, context: CliContext) -> dict:
351
440
  return context.builder.portal_list(profile=args.profile)
352
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
+ )
@@ -8,8 +8,11 @@ from ..tools.ai_builder_tools import AiBuilderTools
8
8
  from ..tools.app_tools import AppTools
9
9
  from ..tools.auth_tools import AuthTools
10
10
  from ..tools.code_block_tools import CodeBlockTools
11
+ from ..tools.feedback_tools import FeedbackTools
12
+ from ..tools.file_tools import FileTools
11
13
  from ..tools.import_tools import ImportTools
12
14
  from ..tools.record_tools import RecordTools
15
+ from ..tools.repository_dev_tools import RepositoryDevTools
13
16
  from ..tools.resource_read_tools import ResourceReadTools
14
17
  from ..tools.task_context_tools import TaskContextTools
15
18
  from ..tools.workspace_tools import WorkspaceTools
@@ -27,7 +30,10 @@ class CliContext:
27
30
  code_block: CodeBlockTools
28
31
  imports: ImportTools
29
32
  task: TaskContextTools
33
+ files: FileTools
34
+ builder_feedback: FeedbackTools
30
35
  builder: AiBuilderTools
36
+ repo: RepositoryDevTools
31
37
 
32
38
  def close(self) -> None:
33
39
  self.backend.close()
@@ -47,5 +53,8 @@ def build_cli_context() -> CliContext:
47
53
  code_block=CodeBlockTools(sessions, backend),
48
54
  imports=ImportTools(sessions, backend),
49
55
  task=TaskContextTools(sessions, backend),
56
+ files=FileTools(sessions, backend),
57
+ builder_feedback=FeedbackTools(backend, mcp_side="App Builder MCP"),
50
58
  builder=AiBuilderTools(sessions, backend),
59
+ repo=RepositoryDevTools(sessions, backend),
51
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"
@@ -170,6 +170,14 @@ def resolve_cli_tool_name(args: Any) -> str | None:
170
170
  if command not in {"builder", "build"}:
171
171
  return None
172
172
  builder_command = getattr(args, "builder_command", None)
173
+ if builder_command == "file":
174
+ return {
175
+ "upload-local": _tool_key(BUILDER_DOMAIN, "file_upload_local"),
176
+ }.get(getattr(args, "builder_file_command", None))
177
+ if builder_command == "feedback":
178
+ return {
179
+ "submit": _tool_key(BUILDER_DOMAIN, "feedback_submit"),
180
+ }.get(getattr(args, "builder_feedback_command", None))
173
181
  if builder_command == "contract":
174
182
  return _tool_key(BUILDER_DOMAIN, "builder_tool_contract")
175
183
  if builder_command == "member":
@@ -202,6 +210,7 @@ def resolve_cli_tool_name(args: Any) -> str | None:
202
210
  return {
203
211
  "resolve": _tool_key(BUILDER_DOMAIN, "app_resolve"),
204
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"),
205
214
  }.get(app_command)
206
215
  if builder_command == "button":
207
216
  return {
@@ -316,6 +325,7 @@ BUILDER_SERVER_METHOD_MAP = {
316
325
  "app_custom_button_delete": _tool_key(BUILDER_DOMAIN, "app_custom_button_delete"),
317
326
  "app_get": _tool_key(BUILDER_DOMAIN, "app_get"),
318
327
  "app_get_fields": _tool_key(BUILDER_DOMAIN, "app_get_fields"),
328
+ "app_repair_code_blocks": _tool_key(BUILDER_DOMAIN, "app_repair_code_blocks"),
319
329
  "app_get_layout": _tool_key(BUILDER_DOMAIN, "app_get_layout"),
320
330
  "app_get_views": _tool_key(BUILDER_DOMAIN, "app_get_views"),
321
331
  "app_get_flow": _tool_key(BUILDER_DOMAIN, "app_get_flow"),
@@ -601,6 +611,7 @@ _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_list",), _trim_works
601
611
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("workspace_select",), _trim_workspace_select)
602
612
  _register_policy((USER_DOMAIN,), ("app_list", "app_search"), _trim_app_search_like)
603
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)
604
615
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("portal_list", "portal_get", "view_get", "chart_get"), _trim_builder_list_like)
605
616
  _register_policy((USER_DOMAIN,), ("file_get_upload_info",), _trim_file_upload_info)
606
617
  _register_policy((USER_DOMAIN, BUILDER_DOMAIN), ("file_upload_local",), _trim_file_upload_local)
@@ -685,6 +696,7 @@ _register_policy(
685
696
  "app_custom_button_update",
686
697
  "app_custom_button_delete",
687
698
  "app_get_fields",
699
+ "app_repair_code_blocks",
688
700
  "app_get_layout",
689
701
  "app_get_views",
690
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)