@qingflow-tech/qingflow-app-user-mcp 1.0.32 → 1.0.34
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/skills/qingflow-app-builder/SKILL.md +26 -6
- package/skills/qingflow-app-builder/references/create-app.md +7 -2
- package/skills/qingflow-app-builder/references/tool-selection.md +8 -1
- package/skills/qingflow-task-ops/SKILL.md +2 -2
- package/src/qingflow_mcp/builder_facade/models.py +30 -0
- package/src/qingflow_mcp/cli/commands/builder.py +33 -5
- package/src/qingflow_mcp/cli/main.py +43 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +138 -9
- package/src/qingflow_mcp/version.py +40 -0
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install:
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
6
|
+
npm install @qingflow-tech/qingflow-app-user-mcp@1.0.34
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.
|
|
12
|
+
npx -y -p @qingflow-tech/qingflow-app-user-mcp@1.0.34 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -32,6 +32,22 @@ Default modeling rules:
|
|
|
32
32
|
- Another business object -> a separate app, not a text field
|
|
33
33
|
- Cross-object links -> relation fields, not text fields
|
|
34
34
|
|
|
35
|
+
## Route First
|
|
36
|
+
|
|
37
|
+
Before any builder write, classify the request:
|
|
38
|
+
|
|
39
|
+
- **Complete system / app package**: the user asks for a system, package, workspace module set, or several related forms/apps. Use `package_apply` for the package, then one `app_schema_apply(apps=[...])` for the app shells and fields. Do not squeeze several business objects into one app.
|
|
40
|
+
- **Single app**: the user names one form/app or gives one `app_key`. Use `app_resolve`/`app_get`, then `app_schema_apply` and the app-scoped apply tools.
|
|
41
|
+
- **Record/user operation**: the user wants to add, edit, delete, approve, or analyze data. Route to the record/task skills instead of builder tools.
|
|
42
|
+
|
|
43
|
+
For complete systems, `apps[]` should use stable `client_key` values. Same-call relation fields may use `target_app_ref` for a client key or `target_app` for another app name. Prefer `target_app_ref` when names may collide.
|
|
44
|
+
|
|
45
|
+
Builder schema inputs should follow agent-intuitive semantics:
|
|
46
|
+
|
|
47
|
+
- icons may be `icon/color`, `icon_name/icon_color`, `icon_config`, or `icon: {name, color}`
|
|
48
|
+
- `single_select` / `multi_select` options may be strings or objects such as `{label, value}`; tools normalize to option labels
|
|
49
|
+
- relation fields need a target plus `display_field` and `visible_fields`
|
|
50
|
+
|
|
35
51
|
## Public Tools You Should Think In
|
|
36
52
|
|
|
37
53
|
- Package: `package_get`, `package_apply`
|
|
@@ -51,7 +67,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
51
67
|
- use `package_apply(...)` for package creation, rename, icon, visibility, grouping, ordering, and app/portal layout
|
|
52
68
|
- Multi-app schema work:
|
|
53
69
|
- use one `app_schema_apply(apps=[...])` / CLI `builder schema apply --apps-file` when creating several apps in one package
|
|
54
|
-
- same-call relation fields may use `target_app_ref` to point at another `apps[].client_key`
|
|
70
|
+
- same-call relation fields may use `target_app_ref` to point at another `apps[].client_key`, or `target_app` to point at another `apps[].app_name`
|
|
55
71
|
- App base permissions:
|
|
56
72
|
- trust `app_get.editability.can_edit_app_base` for app base-info writes like app name, icon, and visibility
|
|
57
73
|
- `can_edit_form` only means schema/form-route capability; it no longer implies app base-info write capability
|
|
@@ -107,11 +123,15 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
107
123
|
|
|
108
124
|
1. Trust the current MCP/session when it is already injected by the runtime; only run auth/workspace recovery after a tool explicitly reports an auth, credential, or workspace error
|
|
109
125
|
2. Confirm whether the task is read-only or write-impacting
|
|
110
|
-
3.
|
|
126
|
+
3. Classify the build scope:
|
|
127
|
+
- complete system/package -> `package_apply` or `package_get`, then one `app_schema_apply(apps=[...])`
|
|
128
|
+
- single app -> `app_resolve` / `app_get`, then app-scoped apply tools
|
|
129
|
+
- record/task/data request -> leave builder and use the matching record/task skill
|
|
130
|
+
4. Resolve the smallest stable target:
|
|
111
131
|
- app-scoped work -> `app_resolve`
|
|
112
132
|
- package-scoped work with known id -> `package_get`
|
|
113
133
|
- portal inventory -> `portal_list`
|
|
114
|
-
|
|
134
|
+
5. Read only the smallest config slice needed:
|
|
115
135
|
- app map -> `app_get` (default entry; includes compact views, charts, custom buttons, and associated resource pool)
|
|
116
136
|
- fields -> `app_get_fields`
|
|
117
137
|
- layout -> `app_get_layout`
|
|
@@ -119,8 +139,8 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
119
139
|
- flow -> `app_get_flow`
|
|
120
140
|
- charts -> `app_get_charts` only when the app_get compact list is not enough
|
|
121
141
|
- portal -> `portal_get`
|
|
122
|
-
|
|
123
|
-
|
|
142
|
+
6. If the public shape is unclear, call `builder_tool_contract`
|
|
143
|
+
7. Apply the smallest patch tool that fits:
|
|
124
144
|
- fields -> `app_schema_apply`
|
|
125
145
|
- layout -> `app_layout_apply`
|
|
126
146
|
- flow -> `app_flow_apply`
|
|
@@ -130,7 +150,7 @@ Treat these as the official surface. Do not default to `package_create`, `packag
|
|
|
130
150
|
- existing charts -> `app_charts_apply.patch_charts`; new/full charts -> `app_charts_apply.upsert_charts`
|
|
131
151
|
- portal -> `portal_apply`
|
|
132
152
|
- package metadata/layout -> `package_apply`
|
|
133
|
-
|
|
153
|
+
8. Use `app_publish_verify` only when the user explicitly wants final publish/live verification or you need a dedicated verification pass
|
|
134
154
|
|
|
135
155
|
## Safe Usage Rules
|
|
136
156
|
|
|
@@ -9,6 +9,7 @@ Do not use this playbook when the user is really asking for a system/package wit
|
|
|
9
9
|
2. create the related apps in one `app_schema_apply(apps=[...])` call
|
|
10
10
|
3. keep package ownership on the public `package_id` path instead of a separate attach step
|
|
11
11
|
4. add same-call relation fields with `target_app_ref` when one new app references another new app
|
|
12
|
+
5. choose explicit non-template `icon + color` for each new package/app
|
|
12
13
|
|
|
13
14
|
Hierarchy reminder:
|
|
14
15
|
|
|
@@ -50,7 +51,8 @@ Create a new package only after the user confirms package creation:
|
|
|
50
51
|
"arguments": {
|
|
51
52
|
"profile": "default",
|
|
52
53
|
"package_name": "研发项目管理",
|
|
53
|
-
"create_if_missing": true
|
|
54
|
+
"create_if_missing": true,
|
|
55
|
+
"icon": {"name": "briefcase", "color": "azure"}
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
```
|
|
@@ -76,6 +78,7 @@ Apply schema for a new app:
|
|
|
76
78
|
"profile": "default",
|
|
77
79
|
"app_name": "客户订单",
|
|
78
80
|
"package_id": 1218950,
|
|
81
|
+
"icon": {"name": "delivery-box-1", "color": "emerald"},
|
|
79
82
|
"create_if_missing": true,
|
|
80
83
|
"publish": true,
|
|
81
84
|
"add_fields": [
|
|
@@ -83,7 +86,7 @@ Apply schema for a new app:
|
|
|
83
86
|
{"name": "客户名称", "type": "text", "required": true},
|
|
84
87
|
{"name": "订单封面", "type": "attachment", "as_data_cover": true},
|
|
85
88
|
{"name": "订单金额", "type": "amount"},
|
|
86
|
-
{"name": "状态", "type": "single_select", "options": ["草稿", "进行中", "已完成"], "required": true}
|
|
89
|
+
{"name": "状态", "type": "single_select", "options": [{"label": "草稿"}, {"label": "进行中"}, {"label": "已完成"}], "required": true}
|
|
87
90
|
],
|
|
88
91
|
"update_fields": [],
|
|
89
92
|
"remove_fields": []
|
|
@@ -105,6 +108,7 @@ Apply schema for multiple apps in one call:
|
|
|
105
108
|
{
|
|
106
109
|
"client_key": "customer",
|
|
107
110
|
"app_name": "客户",
|
|
111
|
+
"icon": {"name": "business-personalcard", "color": "emerald"},
|
|
108
112
|
"add_fields": [
|
|
109
113
|
{"name": "客户名称", "type": "text", "required": true, "as_data_title": true}
|
|
110
114
|
]
|
|
@@ -112,6 +116,7 @@ Apply schema for multiple apps in one call:
|
|
|
112
116
|
{
|
|
113
117
|
"client_key": "order",
|
|
114
118
|
"app_name": "订单",
|
|
119
|
+
"icon": {"name": "delivery-box-1", "color": "blue"},
|
|
115
120
|
"add_fields": [
|
|
116
121
|
{"name": "订单编号", "type": "text", "required": true, "as_data_title": true},
|
|
117
122
|
{
|
|
@@ -19,6 +19,12 @@ Before picking tools, decide which layer the request targets:
|
|
|
19
19
|
|
|
20
20
|
If the user asks for multiple forms/modules that relate to each other, this is a package-level multi-app task, not a single-app create.
|
|
21
21
|
|
|
22
|
+
Use this split consistently:
|
|
23
|
+
|
|
24
|
+
- Complete system/package: `package_apply` first, then one `app_schema_apply(apps=[...])`.
|
|
25
|
+
- Single app: `app_resolve/app_get` first, then app-scoped apply tools.
|
|
26
|
+
- Record/task/data operation: leave builder and use record/task skills.
|
|
27
|
+
|
|
22
28
|
## Resolve
|
|
23
29
|
|
|
24
30
|
- `package_get`: read one known package by `package_id`; it reads package `baseInfo` first and may warn `PACKAGE_DETAIL_READ_DEGRADED` when the richer detail endpoint needs package edit/add-app permission. Treat that warning as degraded detail, not as package-read failure.
|
|
@@ -65,7 +71,7 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
|
|
|
65
71
|
- Create a brand new package, then create one app in it:
|
|
66
72
|
`package_apply(create_if_missing=true) -> app_schema_apply`
|
|
67
73
|
- Create a brand new multi-app system/package:
|
|
68
|
-
`package_apply(create_if_missing=true) -> app_schema_apply(apps[])`
|
|
74
|
+
`package_apply(create_if_missing=true) -> app_schema_apply(apps[])`; use `apps[].client_key` plus `target_app_ref`, or `target_app` when referencing by app name
|
|
69
75
|
- Update fields on an existing app:
|
|
70
76
|
`app_resolve -> app_get_fields -> app_schema_apply`
|
|
71
77
|
- Tidy layout:
|
|
@@ -97,3 +103,4 @@ For object-level updates, the safe partial syntax is `patch_*` with the object's
|
|
|
97
103
|
- Do not omit assignees on approval/fill/copy nodes
|
|
98
104
|
- Do not patch preset flows with brand new approval/fill node ids unless you are intentionally replacing the skeleton; reuse preset ids like `approve_1` and `fill_1`
|
|
99
105
|
- Do not guess role ids, member ids, or editable field ids; resolve names first
|
|
106
|
+
- Do not force agent-authored schema into backend-internal names when public aliases exist: icons may use `icon_config` / `icon:{name,color}`, options may use `{label,value}`, and same-call relation targets may use `target_app_ref` / `target_app`.
|
|
@@ -62,7 +62,7 @@ Use one of these two modes:
|
|
|
62
62
|
2. Discover the exact target with `task_list`
|
|
63
63
|
3. If the target or action requirements are ambiguous, read `task_get`; otherwise go straight to `task_action_execute`
|
|
64
64
|
4. Execute through `task_action_execute`
|
|
65
|
-
5. After actions, report
|
|
65
|
+
5. After actions, report whether it succeeded, the `task_id`, the executed action, the final route/status, and any warnings
|
|
66
66
|
|
|
67
67
|
## Task-Center Rules
|
|
68
68
|
|
|
@@ -111,7 +111,7 @@ Use one of these two modes:
|
|
|
111
111
|
- Do not execute `task_action_execute` until the user explicitly confirms the chosen action
|
|
112
112
|
- Exception: if the user has already explicitly authorized a concrete action on exact targets, you may execute directly after exact target resolution
|
|
113
113
|
- Avoid actions on ambiguous tasks or records
|
|
114
|
-
- Summarize the final action
|
|
114
|
+
- Summarize the final action by `task_id`; include `app_key`, `record_id`, or `workflow_node_id` only as read-only context when the tool returns them, not as the action locator
|
|
115
115
|
- `reject` requires `payload.audit_feedback`
|
|
116
116
|
- For approve/reject, trust the current task detail or an explicit frontend-provided `formId`; app baseInfo is only a fallback. A baseInfo `40002` is not final task-action denial when `formId` is already known.
|
|
117
117
|
- `save_only` requires non-empty `fields` and is only available when the backend exposes editable fields for the current node
|
|
@@ -2292,6 +2292,8 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2292
2292
|
payload = dict(value)
|
|
2293
2293
|
if "fields" in payload and "subfields" not in payload:
|
|
2294
2294
|
payload["subfields"] = payload.pop("fields")
|
|
2295
|
+
if "options" in payload:
|
|
2296
|
+
payload["options"] = _normalize_field_options(payload.get("options"))
|
|
2295
2297
|
raw_type = payload.get("type")
|
|
2296
2298
|
if isinstance(raw_type, int):
|
|
2297
2299
|
normalized_from_id = FIELD_TYPE_ID_ALIASES.get(raw_type)
|
|
@@ -2324,6 +2326,34 @@ def _normalize_field_payload(value: Any) -> Any:
|
|
|
2324
2326
|
return payload
|
|
2325
2327
|
|
|
2326
2328
|
|
|
2329
|
+
def _normalize_field_options(value: Any) -> Any:
|
|
2330
|
+
if not isinstance(value, list):
|
|
2331
|
+
return value
|
|
2332
|
+
normalized: list[str] = []
|
|
2333
|
+
for item in value:
|
|
2334
|
+
if isinstance(item, str):
|
|
2335
|
+
normalized.append(item)
|
|
2336
|
+
continue
|
|
2337
|
+
if isinstance(item, (int, float, bool)):
|
|
2338
|
+
normalized.append(str(item))
|
|
2339
|
+
continue
|
|
2340
|
+
if isinstance(item, dict):
|
|
2341
|
+
label = (
|
|
2342
|
+
item.get("label")
|
|
2343
|
+
or item.get("name")
|
|
2344
|
+
or item.get("title")
|
|
2345
|
+
or item.get("value")
|
|
2346
|
+
or item.get("text")
|
|
2347
|
+
or item.get("optValue")
|
|
2348
|
+
or item.get("optName")
|
|
2349
|
+
)
|
|
2350
|
+
if label is not None:
|
|
2351
|
+
normalized.append(str(label))
|
|
2352
|
+
continue
|
|
2353
|
+
normalized.append(str(item))
|
|
2354
|
+
return normalized
|
|
2355
|
+
|
|
2356
|
+
|
|
2327
2357
|
def _slugify_title(title: str) -> str:
|
|
2328
2358
|
normalized = "".join(ch.lower() if ch.isalnum() else "_" for ch in str(title or ""))
|
|
2329
2359
|
collapsed = "_".join(part for part in normalized.split("_") if part)
|
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
|
|
5
5
|
from ..context import CliContext
|
|
6
6
|
from .common import load_list_arg, load_object_arg, raise_config_error, require_list_arg
|
|
7
|
+
from ..json_io import load_json_value
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -476,26 +477,30 @@ def _handle_chart_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
476
477
|
|
|
477
478
|
|
|
478
479
|
def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
479
|
-
|
|
480
|
+
apps_payload = _load_apps_file_arg(args.apps_file)
|
|
481
|
+
apps = apps_payload["apps"]
|
|
482
|
+
package_id = args.package_id
|
|
483
|
+
if package_id is None and apps_payload.get("package_id") is not None:
|
|
484
|
+
package_id = int(apps_payload["package_id"])
|
|
480
485
|
if args.apps_file:
|
|
481
486
|
if not apps:
|
|
482
487
|
raise_config_error(
|
|
483
488
|
"schema apply multi-app mode requires a non-empty --apps-file.",
|
|
484
|
-
fix_hint="Pass a JSON array with at least one app item.",
|
|
489
|
+
fix_hint="Pass a JSON array, or a JSON object like {\"package_id\":1001,\"apps\":[...]} with at least one app item.",
|
|
485
490
|
)
|
|
486
491
|
if args.app_key or args.app_name or args.app_title or args.add_fields_file or args.update_fields_file or args.remove_fields_file:
|
|
487
492
|
raise_config_error(
|
|
488
493
|
"schema apply multi-app mode accepts --package-id/--create-if-missing plus --apps-file only.",
|
|
489
494
|
fix_hint="Use `--apps-file` for batch mode, or remove `--apps-file` and use the single-app arguments.",
|
|
490
495
|
)
|
|
491
|
-
if
|
|
496
|
+
if package_id is None:
|
|
492
497
|
raise_config_error(
|
|
493
498
|
"schema apply multi-app mode requires --package-id.",
|
|
494
|
-
fix_hint="Pass `--package-id`
|
|
499
|
+
fix_hint="Pass `--package-id`, or put `package_id` at the top level of --apps-file. `package_name` alone does not create the package here; run `builder package apply` first.",
|
|
495
500
|
)
|
|
496
501
|
return context.builder.app_schema_apply(
|
|
497
502
|
profile=args.profile,
|
|
498
|
-
package_id=
|
|
503
|
+
package_id=package_id,
|
|
499
504
|
visibility=load_object_arg(args.visibility_file, option_name="--visibility-file"),
|
|
500
505
|
create_if_missing=bool(args.create_if_missing),
|
|
501
506
|
publish=bool(args.publish),
|
|
@@ -537,6 +542,29 @@ def _handle_schema_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
537
542
|
)
|
|
538
543
|
|
|
539
544
|
|
|
545
|
+
def _load_apps_file_arg(path: str | None) -> dict[str, object]:
|
|
546
|
+
if not path:
|
|
547
|
+
return {"apps": []}
|
|
548
|
+
payload = load_json_value(path, option_name="--apps-file")
|
|
549
|
+
if isinstance(payload, list):
|
|
550
|
+
return {"apps": payload}
|
|
551
|
+
if isinstance(payload, dict):
|
|
552
|
+
apps = payload.get("apps")
|
|
553
|
+
if not isinstance(apps, list):
|
|
554
|
+
raise_config_error(
|
|
555
|
+
"--apps-file JSON object requires an apps array.",
|
|
556
|
+
fix_hint="Use {\"package_id\":1001,\"apps\":[...]} or pass a raw JSON array.",
|
|
557
|
+
)
|
|
558
|
+
result: dict[str, object] = {"apps": apps}
|
|
559
|
+
if payload.get("package_id") is not None:
|
|
560
|
+
result["package_id"] = payload.get("package_id")
|
|
561
|
+
return result
|
|
562
|
+
raise_config_error(
|
|
563
|
+
"--apps-file must be a JSON array or an object containing apps.",
|
|
564
|
+
fix_hint="Use [{...}] or {\"package_id\":1001,\"apps\":[...]}",
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
|
|
540
568
|
def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
|
|
541
569
|
return context.builder.app_layout_apply(
|
|
542
570
|
profile=args.profile,
|
|
@@ -9,6 +9,7 @@ from ..errors import QingflowApiError, backend_code_value_int, message_looks_lik
|
|
|
9
9
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
10
10
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
11
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
12
|
+
from ..version import get_cli_version
|
|
12
13
|
from .context import CliContext, build_cli_context
|
|
13
14
|
from .formatters import emit_json_result, emit_text_result
|
|
14
15
|
from .commands import register_all_commands
|
|
@@ -34,7 +35,10 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
34
35
|
parser = _QingflowArgumentParser(prog="qingflow", description="Qingflow CLI")
|
|
35
36
|
parser.add_argument("--profile", default="default", help="会话 profile,默认 default")
|
|
36
37
|
parser.add_argument("--json", action="store_true", help="输出 JSON")
|
|
38
|
+
parser.add_argument("--version", action="store_true", help="输出 Qingflow CLI 版本")
|
|
37
39
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
40
|
+
version_parser = subparsers.add_parser("version", help="输出 Qingflow CLI 版本")
|
|
41
|
+
version_parser.set_defaults(handler=_handle_version, format_hint="version")
|
|
38
42
|
register_all_commands(subparsers)
|
|
39
43
|
return parser
|
|
40
44
|
|
|
@@ -54,6 +58,8 @@ def run(
|
|
|
54
58
|
err = stderr or sys.stderr
|
|
55
59
|
parser = build_parser()
|
|
56
60
|
normalized_argv = _normalize_global_args(list(argv) if argv is not None else sys.argv[1:])
|
|
61
|
+
if "--version" in normalized_argv:
|
|
62
|
+
return _emit_version(json_mode=_should_force_json_output_argv_for_version(normalized_argv), stdout=out)
|
|
57
63
|
try:
|
|
58
64
|
args = parser.parse_args(normalized_argv)
|
|
59
65
|
except _CliArgumentError as exc:
|
|
@@ -76,6 +82,8 @@ def run(
|
|
|
76
82
|
setattr(args, "_stdin", sys.stdin)
|
|
77
83
|
setattr(args, "_stdout_stream", out)
|
|
78
84
|
setattr(args, "_stderr_stream", err)
|
|
85
|
+
if getattr(args, "command", "") == "version":
|
|
86
|
+
return _emit_version(json_mode=bool(args.json), stdout=out)
|
|
79
87
|
handler = getattr(args, "handler", None)
|
|
80
88
|
if handler is None:
|
|
81
89
|
parser.print_help(out)
|
|
@@ -111,6 +119,37 @@ def run(
|
|
|
111
119
|
return exit_code
|
|
112
120
|
|
|
113
121
|
|
|
122
|
+
def _handle_version(_args: argparse.Namespace, _context: CliContext) -> dict[str, Any]:
|
|
123
|
+
version = get_cli_version()
|
|
124
|
+
return {
|
|
125
|
+
"ok": True,
|
|
126
|
+
"status": "success",
|
|
127
|
+
"version": version,
|
|
128
|
+
"package": "@qingflow-tech/qingflow-cli",
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _emit_version(*, json_mode: bool, stdout: TextIO) -> int:
|
|
133
|
+
version = get_cli_version()
|
|
134
|
+
if json_mode:
|
|
135
|
+
emit_json_result(
|
|
136
|
+
{
|
|
137
|
+
"ok": True,
|
|
138
|
+
"status": "success",
|
|
139
|
+
"version": version,
|
|
140
|
+
"package": "@qingflow-tech/qingflow-cli",
|
|
141
|
+
},
|
|
142
|
+
stream=stdout,
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
stdout.write(f"{version}\n")
|
|
146
|
+
return 0
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _should_force_json_output_argv_for_version(argv: list[str]) -> bool:
|
|
150
|
+
return "--json" in argv
|
|
151
|
+
|
|
152
|
+
|
|
114
153
|
def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
115
154
|
global_args: list[str] = []
|
|
116
155
|
remaining: list[str] = []
|
|
@@ -121,6 +160,10 @@ def _normalize_global_args(argv: list[str]) -> list[str]:
|
|
|
121
160
|
global_args.append(token)
|
|
122
161
|
index += 1
|
|
123
162
|
continue
|
|
163
|
+
if token == "--version":
|
|
164
|
+
global_args.append(token)
|
|
165
|
+
index += 1
|
|
166
|
+
continue
|
|
124
167
|
if token == "--profile":
|
|
125
168
|
global_args.append(token)
|
|
126
169
|
if index + 1 >= len(argv):
|
|
@@ -146,8 +146,11 @@ class AiBuilderTools(ToolBase):
|
|
|
146
146
|
package_id: int | None = None,
|
|
147
147
|
package_name: str | None = None,
|
|
148
148
|
create_if_missing: bool = False,
|
|
149
|
-
icon: str | None = None,
|
|
149
|
+
icon: str | JSONObject | None = None,
|
|
150
150
|
color: str | None = None,
|
|
151
|
+
icon_name: str | None = None,
|
|
152
|
+
icon_color: str | None = None,
|
|
153
|
+
icon_config: JSONObject | None = None,
|
|
151
154
|
visibility: JSONObject | None = None,
|
|
152
155
|
items: list[dict] | None = None,
|
|
153
156
|
allow_detach: bool = False,
|
|
@@ -159,6 +162,9 @@ class AiBuilderTools(ToolBase):
|
|
|
159
162
|
create_if_missing=create_if_missing,
|
|
160
163
|
icon=icon,
|
|
161
164
|
color=color,
|
|
165
|
+
icon_name=icon_name,
|
|
166
|
+
icon_color=icon_color,
|
|
167
|
+
icon_config=icon_config,
|
|
162
168
|
visibility=visibility,
|
|
163
169
|
items=items or None,
|
|
164
170
|
allow_detach=allow_detach,
|
|
@@ -364,8 +370,11 @@ class AiBuilderTools(ToolBase):
|
|
|
364
370
|
package_id: int | None = None,
|
|
365
371
|
app_name: str = "",
|
|
366
372
|
app_title: str = "",
|
|
367
|
-
icon: str = "",
|
|
373
|
+
icon: str | JSONObject = "",
|
|
368
374
|
color: str = "",
|
|
375
|
+
icon_name: str | None = None,
|
|
376
|
+
icon_color: str | None = None,
|
|
377
|
+
icon_config: JSONObject | None = None,
|
|
369
378
|
visibility: JSONObject | None = None,
|
|
370
379
|
create_if_missing: bool = False,
|
|
371
380
|
publish: bool = True,
|
|
@@ -423,6 +432,9 @@ class AiBuilderTools(ToolBase):
|
|
|
423
432
|
app_title=app_title,
|
|
424
433
|
icon=icon,
|
|
425
434
|
color=color,
|
|
435
|
+
icon_name=icon_name,
|
|
436
|
+
icon_color=icon_color,
|
|
437
|
+
icon_config=icon_config,
|
|
426
438
|
visibility=visibility,
|
|
427
439
|
create_if_missing=create_if_missing,
|
|
428
440
|
publish=publish,
|
|
@@ -733,13 +745,23 @@ class AiBuilderTools(ToolBase):
|
|
|
733
745
|
package_id: int | None = None,
|
|
734
746
|
package_name: str | None = None,
|
|
735
747
|
create_if_missing: bool = False,
|
|
736
|
-
icon: str | None = None,
|
|
748
|
+
icon: str | JSONObject | None = None,
|
|
737
749
|
color: str | None = None,
|
|
750
|
+
icon_name: str | None = None,
|
|
751
|
+
icon_color: str | None = None,
|
|
752
|
+
icon_config: JSONObject | None = None,
|
|
738
753
|
visibility: JSONObject | None = None,
|
|
739
754
|
items: list[dict] | None = None,
|
|
740
755
|
allow_detach: bool = False,
|
|
741
756
|
) -> JSONObject:
|
|
742
757
|
"""执行分组与包相关逻辑。"""
|
|
758
|
+
icon, color = _normalize_builder_icon_args(
|
|
759
|
+
icon=icon,
|
|
760
|
+
color=color,
|
|
761
|
+
icon_name=icon_name,
|
|
762
|
+
icon_color=icon_color,
|
|
763
|
+
icon_config=icon_config,
|
|
764
|
+
)
|
|
743
765
|
visibility_patch = None
|
|
744
766
|
if visibility is not None:
|
|
745
767
|
try:
|
|
@@ -1650,8 +1672,11 @@ class AiBuilderTools(ToolBase):
|
|
|
1650
1672
|
package_id: int | None = None,
|
|
1651
1673
|
app_name: str = "",
|
|
1652
1674
|
app_title: str = "",
|
|
1653
|
-
icon: str = "",
|
|
1675
|
+
icon: str | JSONObject = "",
|
|
1654
1676
|
color: str = "",
|
|
1677
|
+
icon_name: str | None = None,
|
|
1678
|
+
icon_color: str | None = None,
|
|
1679
|
+
icon_config: JSONObject | None = None,
|
|
1655
1680
|
visibility: JSONObject | None = None,
|
|
1656
1681
|
create_if_missing: bool = False,
|
|
1657
1682
|
publish: bool = True,
|
|
@@ -1661,7 +1686,15 @@ class AiBuilderTools(ToolBase):
|
|
|
1661
1686
|
apps: list[JSONObject] | None = None,
|
|
1662
1687
|
) -> JSONObject:
|
|
1663
1688
|
"""执行应用相关逻辑。"""
|
|
1689
|
+
icon, color = _normalize_builder_icon_args(
|
|
1690
|
+
icon=icon,
|
|
1691
|
+
color=color,
|
|
1692
|
+
icon_name=icon_name,
|
|
1693
|
+
icon_color=icon_color,
|
|
1694
|
+
icon_config=icon_config,
|
|
1695
|
+
)
|
|
1664
1696
|
if apps:
|
|
1697
|
+
apps = [_normalize_schema_app_item(item) if isinstance(item, dict) else item for item in apps]
|
|
1665
1698
|
result = self._app_schema_apply_multi(
|
|
1666
1699
|
profile=profile,
|
|
1667
1700
|
package_id=package_id,
|
|
@@ -1801,6 +1834,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1801
1834
|
)
|
|
1802
1835
|
|
|
1803
1836
|
client_key_to_app_key: dict[str, str] = {}
|
|
1837
|
+
app_name_to_app_key: dict[str, str] = {}
|
|
1804
1838
|
created_app_keys: list[str] = []
|
|
1805
1839
|
results: list[JSONObject] = []
|
|
1806
1840
|
any_write_executed = False
|
|
@@ -1864,11 +1898,14 @@ class AiBuilderTools(ToolBase):
|
|
|
1864
1898
|
created_app_keys.append(resolved_key)
|
|
1865
1899
|
if client_key:
|
|
1866
1900
|
client_key_to_app_key[client_key] = resolved_key
|
|
1901
|
+
resolved_name = str(public_shell.get("app_name_after") or public_shell.get("app_name") or app_name or "").strip()
|
|
1902
|
+
if resolved_name:
|
|
1903
|
+
app_name_to_app_key[resolved_name] = resolved_key
|
|
1867
1904
|
results.append({
|
|
1868
1905
|
"index": index,
|
|
1869
1906
|
"row_number": index + 1,
|
|
1870
1907
|
"client_key": client_key or None,
|
|
1871
|
-
"app_name":
|
|
1908
|
+
"app_name": resolved_name or None,
|
|
1872
1909
|
"app_key": resolved_key,
|
|
1873
1910
|
"status": "shell_ready",
|
|
1874
1911
|
"created": bool(public_shell.get("created")),
|
|
@@ -1888,7 +1925,7 @@ class AiBuilderTools(ToolBase):
|
|
|
1888
1925
|
item = deepcopy(raw_item)
|
|
1889
1926
|
app_key = str(existing.get("app_key") or "").strip()
|
|
1890
1927
|
try:
|
|
1891
|
-
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key)
|
|
1928
|
+
compiled_item = _compile_multi_app_schema_item_refs(item, client_key_to_app_key, app_name_to_app_key)
|
|
1892
1929
|
except ValueError as error:
|
|
1893
1930
|
final_items.append({
|
|
1894
1931
|
**{key: existing.get(key) for key in ("index", "row_number", "client_key", "app_name", "app_key", "created")},
|
|
@@ -2786,8 +2823,58 @@ def _multi_app_item_failure(index: int, item: object, error_code: str, message:
|
|
|
2786
2823
|
}
|
|
2787
2824
|
|
|
2788
2825
|
|
|
2789
|
-
def
|
|
2826
|
+
def _normalize_builder_icon_args(
|
|
2827
|
+
*,
|
|
2828
|
+
icon: str | JSONObject | None,
|
|
2829
|
+
color: str | None,
|
|
2830
|
+
icon_name: str | None = None,
|
|
2831
|
+
icon_color: str | None = None,
|
|
2832
|
+
icon_config: JSONObject | None = None,
|
|
2833
|
+
) -> tuple[str | None, str | None]:
|
|
2834
|
+
payloads: list[JSONObject] = []
|
|
2835
|
+
if isinstance(icon_config, dict):
|
|
2836
|
+
payloads.append(icon_config)
|
|
2837
|
+
if isinstance(icon, dict):
|
|
2838
|
+
payloads.append(icon)
|
|
2839
|
+
|
|
2840
|
+
normalized_icon = None if isinstance(icon, dict) else icon
|
|
2841
|
+
normalized_color = color
|
|
2842
|
+
for payload in payloads:
|
|
2843
|
+
normalized_icon = normalized_icon or payload.get("icon") or payload.get("icon_name") or payload.get("iconName") or payload.get("name")
|
|
2844
|
+
normalized_color = normalized_color or payload.get("color") or payload.get("icon_color") or payload.get("iconColor")
|
|
2845
|
+
normalized_icon = normalized_icon or icon_name
|
|
2846
|
+
normalized_color = normalized_color or icon_color
|
|
2847
|
+
return (
|
|
2848
|
+
str(normalized_icon).strip() if normalized_icon is not None else None,
|
|
2849
|
+
str(normalized_color).strip() if normalized_color is not None else None,
|
|
2850
|
+
)
|
|
2851
|
+
|
|
2852
|
+
|
|
2853
|
+
def _normalize_schema_app_item(item: JSONObject) -> JSONObject:
|
|
2854
|
+
normalized = deepcopy(item)
|
|
2855
|
+
icon, color = _normalize_builder_icon_args(
|
|
2856
|
+
icon=normalized.get("icon"),
|
|
2857
|
+
color=normalized.get("color"),
|
|
2858
|
+
icon_name=normalized.get("icon_name") or normalized.get("iconName"),
|
|
2859
|
+
icon_color=normalized.get("icon_color") or normalized.get("iconColor"),
|
|
2860
|
+
icon_config=normalized.get("icon_config") or normalized.get("iconConfig"),
|
|
2861
|
+
)
|
|
2862
|
+
if icon:
|
|
2863
|
+
normalized["icon"] = icon
|
|
2864
|
+
if color:
|
|
2865
|
+
normalized["color"] = color
|
|
2866
|
+
for key in ("icon_name", "iconName", "icon_color", "iconColor", "icon_config", "iconConfig"):
|
|
2867
|
+
normalized.pop(key, None)
|
|
2868
|
+
return normalized
|
|
2869
|
+
|
|
2870
|
+
|
|
2871
|
+
def _compile_multi_app_schema_item_refs(
|
|
2872
|
+
item: JSONObject,
|
|
2873
|
+
client_key_to_app_key: dict[str, str],
|
|
2874
|
+
app_name_to_app_key: dict[str, str] | None = None,
|
|
2875
|
+
) -> JSONObject:
|
|
2790
2876
|
compiled = deepcopy(item)
|
|
2877
|
+
app_name_to_app_key = app_name_to_app_key or {}
|
|
2791
2878
|
|
|
2792
2879
|
def visit(value):
|
|
2793
2880
|
if isinstance(value, list):
|
|
@@ -2807,6 +2894,13 @@ def _compile_multi_app_schema_item_refs(item: JSONObject, client_key_to_app_key:
|
|
|
2807
2894
|
if not target_app_key:
|
|
2808
2895
|
raise ValueError(f"target_app_ref '{ref_key}' did not match any apps[].client_key")
|
|
2809
2896
|
payload["target_app_key"] = target_app_key
|
|
2897
|
+
target_app = payload.pop("target_app", None) or payload.pop("targetApp", None)
|
|
2898
|
+
if target_app is not None and not str(payload.get("target_app_key") or "").strip():
|
|
2899
|
+
target_name = str(target_app or "").strip()
|
|
2900
|
+
target_app_key = app_name_to_app_key.get(target_name)
|
|
2901
|
+
if not target_app_key:
|
|
2902
|
+
raise ValueError(f"target_app '{target_name}' did not match any apps[].app_name in the same call")
|
|
2903
|
+
payload["target_app_key"] = target_app_key
|
|
2810
2904
|
return payload
|
|
2811
2905
|
|
|
2812
2906
|
return visit(compiled)
|
|
@@ -2855,7 +2949,7 @@ def _contains_multi_app_target_ref(value: object) -> bool:
|
|
|
2855
2949
|
if not isinstance(value, dict):
|
|
2856
2950
|
return False
|
|
2857
2951
|
for key, entry in value.items():
|
|
2858
|
-
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey"}:
|
|
2952
|
+
if key in {"target_app_ref", "targetAppRef", "target_app_client_key", "targetAppClientKey", "target_app", "targetApp"}:
|
|
2859
2953
|
return True
|
|
2860
2954
|
if _contains_multi_app_target_ref(entry):
|
|
2861
2955
|
return True
|
|
@@ -4054,13 +4148,29 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4054
4148
|
},
|
|
4055
4149
|
},
|
|
4056
4150
|
"package_apply": {
|
|
4057
|
-
"allowed_keys": [
|
|
4151
|
+
"allowed_keys": [
|
|
4152
|
+
"package_id",
|
|
4153
|
+
"package_name",
|
|
4154
|
+
"create_if_missing",
|
|
4155
|
+
"icon",
|
|
4156
|
+
"color",
|
|
4157
|
+
"icon_name",
|
|
4158
|
+
"icon_color",
|
|
4159
|
+
"icon_config",
|
|
4160
|
+
"visibility",
|
|
4161
|
+
"items",
|
|
4162
|
+
"allow_detach",
|
|
4163
|
+
],
|
|
4058
4164
|
"aliases": {
|
|
4059
4165
|
"packageId": "package_id",
|
|
4060
4166
|
"packageName": "package_name",
|
|
4061
4167
|
"createIfMissing": "create_if_missing",
|
|
4062
4168
|
"iconName": "icon",
|
|
4063
4169
|
"iconColor": "color",
|
|
4170
|
+
"icon_config.name": "icon",
|
|
4171
|
+
"icon_config.icon_name": "icon",
|
|
4172
|
+
"icon_config.color": "color",
|
|
4173
|
+
"icon_config.icon_color": "color",
|
|
4064
4174
|
"allowDetach": "allow_detach",
|
|
4065
4175
|
},
|
|
4066
4176
|
"allowed_values": {
|
|
@@ -4072,6 +4182,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4072
4182
|
"execution_notes": [
|
|
4073
4183
|
"create or update package metadata, visibility, grouping, and ordering in one call",
|
|
4074
4184
|
"creating a package requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4185
|
+
"icon can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
|
|
4075
4186
|
"updating a package preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4076
4187
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4077
4188
|
"metadata keys omitted on update are preserved",
|
|
@@ -4575,6 +4686,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4575
4686
|
"app_title",
|
|
4576
4687
|
"icon",
|
|
4577
4688
|
"color",
|
|
4689
|
+
"icon_name",
|
|
4690
|
+
"icon_color",
|
|
4691
|
+
"icon_config",
|
|
4578
4692
|
"visibility",
|
|
4579
4693
|
"create_if_missing",
|
|
4580
4694
|
"publish",
|
|
@@ -4587,11 +4701,15 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4587
4701
|
"apps[].app_name",
|
|
4588
4702
|
"apps[].icon",
|
|
4589
4703
|
"apps[].color",
|
|
4704
|
+
"apps[].icon_name",
|
|
4705
|
+
"apps[].icon_color",
|
|
4706
|
+
"apps[].icon_config",
|
|
4590
4707
|
"apps[].visibility",
|
|
4591
4708
|
"apps[].add_fields",
|
|
4592
4709
|
"apps[].update_fields",
|
|
4593
4710
|
"apps[].remove_fields",
|
|
4594
4711
|
"apps[].add_fields[].target_app_ref",
|
|
4712
|
+
"apps[].add_fields[].target_app",
|
|
4595
4713
|
],
|
|
4596
4714
|
"aliases": {
|
|
4597
4715
|
"app_title": "app_name",
|
|
@@ -4601,8 +4719,16 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4601
4719
|
"apps[].appKey": "apps[].app_key",
|
|
4602
4720
|
"apps[].appName": "apps[].app_name",
|
|
4603
4721
|
"apps[].appTitle": "apps[].app_name",
|
|
4722
|
+
"apps[].iconName": "apps[].icon",
|
|
4723
|
+
"apps[].iconColor": "apps[].color",
|
|
4724
|
+
"apps[].icon_config.name": "apps[].icon",
|
|
4725
|
+
"apps[].icon_config.color": "apps[].color",
|
|
4604
4726
|
"field.targetAppRef": "field.target_app_ref",
|
|
4605
4727
|
"field.targetAppClientKey": "field.target_app_ref",
|
|
4728
|
+
"field.targetApp": "field.target_app",
|
|
4729
|
+
"field.options[].label": "field.options[]",
|
|
4730
|
+
"field.options[].value": "field.options[]",
|
|
4731
|
+
"field.options[].optValue": "field.options[]",
|
|
4606
4732
|
"field.title": "field.name",
|
|
4607
4733
|
"field.label": "field.name",
|
|
4608
4734
|
"field.fields": "field.subfields",
|
|
@@ -4642,10 +4768,13 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
|
|
|
4642
4768
|
"create mode follows backend CreateAppBean: package add_app permission is checked on the target package; package edit_app is not required for the create precheck",
|
|
4643
4769
|
"multi-app mode: pass package_id + create_if_missing + apps[]; do not mix apps with top-level app_key/app_name/add_fields/update_fields/remove_fields",
|
|
4644
4770
|
"multi-app relation fields may use target_app_ref to point at another apps[].client_key; the tool creates/resolves app shells first and compiles it to target_app_key",
|
|
4771
|
+
"multi-app relation fields may also use target_app with another apps[].app_name; prefer target_app_ref/client_key when names may collide",
|
|
4645
4772
|
"multi-app mode is not transactional; read created_app_keys and apps[].status before retrying, and retry only failed app items",
|
|
4646
4773
|
"create mode defaults new app visibility to workspace/not when visibility is omitted; edit mode preserves current visibility when omitted",
|
|
4647
4774
|
"create mode requires explicit icon + color; icon=template is blocked because it is too generic",
|
|
4775
|
+
"icon can be passed as icon/color, icon_name/icon_color, icon_config, or icon={name,color}; all forms normalize to icon/color",
|
|
4648
4776
|
"multi-app create mode requires each new app item to include a distinct non-template icon and a valid color",
|
|
4777
|
+
"single_select and multi_select options accept strings or objects such as {label,value}; builder normalizes them to option labels before writing",
|
|
4649
4778
|
"edit mode preserves existing icon/color when omitted; explicit icon/color values are still validated",
|
|
4650
4779
|
"call workspace_icon_catalog_get or `qingflow --json builder icon catalog` for supported icon/color candidates",
|
|
4651
4780
|
*_VISIBILITY_EXECUTION_NOTES,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from importlib import metadata
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_cli_version() -> str:
|
|
9
|
+
package_json_version = _find_package_json_version()
|
|
10
|
+
if package_json_version:
|
|
11
|
+
return package_json_version
|
|
12
|
+
try:
|
|
13
|
+
return metadata.version("qingflow-mcp")
|
|
14
|
+
except metadata.PackageNotFoundError:
|
|
15
|
+
return "0+local"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _find_package_json_version() -> str | None:
|
|
19
|
+
current = Path(__file__).resolve()
|
|
20
|
+
for parent in current.parents:
|
|
21
|
+
package_json = parent / "package.json"
|
|
22
|
+
if not package_json.exists():
|
|
23
|
+
continue
|
|
24
|
+
try:
|
|
25
|
+
payload = json.loads(package_json.read_text(encoding="utf-8"))
|
|
26
|
+
except (OSError, json.JSONDecodeError):
|
|
27
|
+
continue
|
|
28
|
+
name = str(payload.get("name") or "")
|
|
29
|
+
version = str(payload.get("version") or "")
|
|
30
|
+
if version and name in {
|
|
31
|
+
"qingflow-mcp-workspace",
|
|
32
|
+
"@qingflow-tech/qingflow-cli",
|
|
33
|
+
"@qingflow-tech/qingflow-app-user-mcp",
|
|
34
|
+
"@qingflow-tech/qingflow-app-builder-mcp",
|
|
35
|
+
"@josephyan/qingflow-cli",
|
|
36
|
+
"@josephyan/qingflow-app-user-mcp",
|
|
37
|
+
"@josephyan/qingflow-app-builder-mcp",
|
|
38
|
+
}:
|
|
39
|
+
return version
|
|
40
|
+
return None
|