@qingflow-tech/qingflow-cli 1.1.51 → 1.1.54
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 +4 -4
- package/npm/lib/runtime.mjs +29 -13
- package/package.json +1 -1
- package/pyproject.toml +8 -2
- package/skills/qingflow-cli/SKILL.md +93 -322
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +3 -2
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
- package/skills/qingflow-cli/reference/builder/50-views.md +192 -191
- package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
- package/skills/qingflow-cli/reference/builder/README.md +1 -1
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
- package/skills/qingflow-cli/reference/data/README.md +32 -0
- package/skills/qingflow-cli/reference/examples/README.md +1 -1
- package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
- package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
- package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
- package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
- package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
- package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
- package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
- package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
- package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
- package/skills/qingflow-common/SKILL.md +7 -4
- package/src/qingflow_mcp/app_form_compat.py +3 -0
- package/src/qingflow_mcp/app_form_validator.py +69 -17
- package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
- package/src/qingflow_mcp/builder_facade/models.py +227 -17
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
- package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
- package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
- package/src/qingflow_mcp/builder_facade/service.py +1709 -518
- package/src/qingflow_mcp/builder_facade/views_extract.py +595 -0
- package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/builder.py +76 -55
- package/src/qingflow_mcp/cli/commands/data.py +134 -0
- package/src/qingflow_mcp/cli/commands/record.py +1 -1
- package/src/qingflow_mcp/cli/context.py +3 -0
- package/src/qingflow_mcp/cli/formatters.py +97 -1
- package/src/qingflow_mcp/public_surface.py +8 -0
- package/src/qingflow_mcp/response_trim.py +11 -2
- package/src/qingflow_mcp/server.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +34 -10
- package/src/qingflow_mcp/server_app_user.py +4 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
- package/src/qingflow_mcp/solution/executor.py +96 -8
- package/src/qingflow_mcp/solution/spec_models.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +218 -398
- package/src/qingflow_mcp/tools/csv_export.py +168 -0
- package/src/qingflow_mcp/tools/data_tools.py +284 -0
- package/src/qingflow_mcp/tools/record_tools.py +11 -46
- package/src/qingflow_mcp/tools/solution_tools.py +2 -0
- package/src/qingflow_mcp/tools/view_tools.py +0 -6
- package/src/qingflow_mcp/tools/view_writer.py +44 -0
- package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
- package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import textwrap
|
|
5
|
+
|
|
6
|
+
from ..context import CliContext
|
|
7
|
+
from .common import load_list_arg, require_list_arg, require_object_arg, raise_config_error
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
11
|
+
parser = subparsers.add_parser(
|
|
12
|
+
"data",
|
|
13
|
+
help="默认应用数据查询与直接增删改",
|
|
14
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
15
|
+
epilog=textwrap.dedent("""
|
|
16
|
+
JSON 文件约定:
|
|
17
|
+
columns.json [101, 102]
|
|
18
|
+
where.json [{"field_id": 101, "op": "eq", "value": "A"}]
|
|
19
|
+
sort.json [{"field_id": 101, "direction": "asc"}]
|
|
20
|
+
items.json [{"fields": {"101": "A"}}]
|
|
21
|
+
fields.json {"101": "B"}
|
|
22
|
+
query/get/insert/update 默认返回 field_capabilities;可用 --no-schema 关闭。
|
|
23
|
+
""").strip(),
|
|
24
|
+
)
|
|
25
|
+
commands = parser.add_subparsers(dest="data_command", required=True, metavar="{query,get,insert,update,delete}")
|
|
26
|
+
|
|
27
|
+
query = commands.add_parser(
|
|
28
|
+
"query", help="分页查询应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
29
|
+
epilog='最小示例:qingflow data query --app-key APP_1 --where-file where.json\n'
|
|
30
|
+
'where.json: [{"field_id": 101, "op": "eq", "value": "A"}]',
|
|
31
|
+
)
|
|
32
|
+
query.add_argument("--app-key", required=True)
|
|
33
|
+
query.add_argument("--columns-file", help='JSON 数组,例如 [101, 102]')
|
|
34
|
+
query.add_argument(
|
|
35
|
+
"--where-file",
|
|
36
|
+
help='JSON 数组;项为 {"field_id":101,"op":"eq","value":"A"},in/not_in 的 value 必须为数组',
|
|
37
|
+
)
|
|
38
|
+
query.add_argument("--sort-file", help='JSON 数组,例如 [{"field_id":101,"direction":"asc"}]')
|
|
39
|
+
query.add_argument("--search", default="")
|
|
40
|
+
query.add_argument("--page", type=int, default=1)
|
|
41
|
+
query.add_argument("--page-size", type=int, default=100)
|
|
42
|
+
query.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
43
|
+
query.add_argument("--all", action="store_true", dest="all_pages")
|
|
44
|
+
query.add_argument("--output", choices=("json", "csv"), default="json")
|
|
45
|
+
query.set_defaults(handler=_handle_query, format_hint="data_query")
|
|
46
|
+
|
|
47
|
+
get = commands.add_parser("get", help="读取单条应用数据")
|
|
48
|
+
get.add_argument("--app-key", required=True)
|
|
49
|
+
get.add_argument("--record-id", required=True)
|
|
50
|
+
get.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
51
|
+
get.set_defaults(handler=_handle_get, format_hint="data_get")
|
|
52
|
+
|
|
53
|
+
insert = commands.add_parser(
|
|
54
|
+
"insert", help="新增应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
55
|
+
epilog='最小示例:items.json = [{"fields": {"101": "A"}}]',
|
|
56
|
+
)
|
|
57
|
+
insert.add_argument("--app-key", required=True)
|
|
58
|
+
insert.add_argument("--items-file", required=True, help='JSON 数组,项为 {"fields":{"字段ID":值}}')
|
|
59
|
+
insert.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
60
|
+
insert.set_defaults(handler=_handle_insert, format_hint="data_insert")
|
|
61
|
+
|
|
62
|
+
update = commands.add_parser(
|
|
63
|
+
"update", help="更新应用数据", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
64
|
+
epilog='最小示例:fields.json = {"101": "B"}',
|
|
65
|
+
)
|
|
66
|
+
update.add_argument("--app-key", required=True)
|
|
67
|
+
update.add_argument("--record-id", required=True)
|
|
68
|
+
update.add_argument("--fields-file", required=True, help='JSON 对象,键为字符串形式 field_id')
|
|
69
|
+
update.add_argument("--no-schema", action="store_false", dest="include_schema", default=True)
|
|
70
|
+
update.set_defaults(handler=_handle_update, format_hint="data_update")
|
|
71
|
+
|
|
72
|
+
delete = commands.add_parser("delete", help="删除应用数据")
|
|
73
|
+
delete.add_argument("--app-key", required=True)
|
|
74
|
+
delete.add_argument(
|
|
75
|
+
"--record-ids", action="append", default=[], metavar="ID[,ID...]",
|
|
76
|
+
help="待删除记录 ID;支持逗号分隔,也可重复传入",
|
|
77
|
+
)
|
|
78
|
+
delete.add_argument("--record-ids-file", help="附加的记录 ID JSON 数组文件")
|
|
79
|
+
delete.set_defaults(handler=_handle_delete, format_hint="data_delete")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _handle_query(args: argparse.Namespace, context: CliContext) -> dict:
|
|
83
|
+
if args.all_pages != (args.output == "csv"):
|
|
84
|
+
raise_config_error(
|
|
85
|
+
"--all 与 --output csv 必须同时使用。",
|
|
86
|
+
fix_hint="使用 `qingflow data query ... --all --output csv`,普通分页查询不要传这两个参数。",
|
|
87
|
+
)
|
|
88
|
+
return context.data.data_query(
|
|
89
|
+
profile=args.profile,
|
|
90
|
+
app_key=args.app_key,
|
|
91
|
+
columns=load_list_arg(args.columns_file, option_name="--columns-file"),
|
|
92
|
+
where=load_list_arg(args.where_file, option_name="--where-file"),
|
|
93
|
+
sort=load_list_arg(args.sort_file, option_name="--sort-file"),
|
|
94
|
+
search=args.search,
|
|
95
|
+
page=args.page,
|
|
96
|
+
page_size=args.page_size,
|
|
97
|
+
include_schema=args.include_schema,
|
|
98
|
+
all_pages=args.all_pages,
|
|
99
|
+
output=args.output,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
104
|
+
return context.data.data_get(
|
|
105
|
+
profile=args.profile, app_key=args.app_key, record_id=args.record_id, include_schema=args.include_schema
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _handle_insert(args: argparse.Namespace, context: CliContext) -> dict:
|
|
110
|
+
return context.data.data_insert(
|
|
111
|
+
profile=args.profile, app_key=args.app_key,
|
|
112
|
+
items=require_list_arg(args.items_file, option_name="--items-file"), include_schema=args.include_schema,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _handle_update(args: argparse.Namespace, context: CliContext) -> dict:
|
|
117
|
+
return context.data.data_update(
|
|
118
|
+
profile=args.profile, app_key=args.app_key, record_id=args.record_id,
|
|
119
|
+
fields=require_object_arg(args.fields_file, option_name="--fields-file"), include_schema=args.include_schema,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _handle_delete(args: argparse.Namespace, context: CliContext) -> dict:
|
|
124
|
+
record_ids = [item.strip() for group in args.record_ids for item in group.split(",") if item.strip()]
|
|
125
|
+
record_ids.extend(load_list_arg(args.record_ids_file, option_name="--record-ids-file"))
|
|
126
|
+
if not record_ids:
|
|
127
|
+
raise_config_error(
|
|
128
|
+
"至少传入一个记录 ID。",
|
|
129
|
+
fix_hint="使用 `--record-ids ID[,ID...]`;也可附加 `--record-ids-file ids.json`。",
|
|
130
|
+
)
|
|
131
|
+
return context.data.data_delete(
|
|
132
|
+
profile=args.profile, app_key=args.app_key,
|
|
133
|
+
record_ids=record_ids,
|
|
134
|
+
)
|
|
@@ -9,7 +9,7 @@ from .common import load_list_arg, load_object_arg, raise_config_error, require_
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
12
|
-
parser = subparsers.add_parser("record", help="
|
|
12
|
+
parser = subparsers.add_parser("record", help="日志、附件、导入、代码块及 record 专项能力")
|
|
13
13
|
record_subparsers = parser.add_subparsers(
|
|
14
14
|
dest="record_command",
|
|
15
15
|
required=True,
|
|
@@ -11,6 +11,7 @@ from ..tools.ai_builder_tools import AiBuilderTools
|
|
|
11
11
|
from ..tools.app_tools import AppTools
|
|
12
12
|
from ..tools.auth_tools import AuthTools
|
|
13
13
|
from ..tools.code_block_tools import CodeBlockTools
|
|
14
|
+
from ..tools.data_tools import DataTools
|
|
14
15
|
from ..tools.export_tools import ExportTools
|
|
15
16
|
from ..tools.feedback_tools import FeedbackTools
|
|
16
17
|
from ..tools.file_tools import FileTools
|
|
@@ -33,6 +34,7 @@ class CliContext:
|
|
|
33
34
|
app: AppTools
|
|
34
35
|
resource: ResourceReadTools
|
|
35
36
|
record: RecordTools
|
|
37
|
+
data: DataTools
|
|
36
38
|
code_block: CodeBlockTools
|
|
37
39
|
imports: ImportTools
|
|
38
40
|
exports: ExportTools
|
|
@@ -64,6 +66,7 @@ def build_cli_context() -> CliContext:
|
|
|
64
66
|
app=AppTools(sessions, backend),
|
|
65
67
|
resource=ResourceReadTools(sessions, backend),
|
|
66
68
|
record=RecordTools(sessions, backend),
|
|
69
|
+
data=DataTools(sessions, backend),
|
|
67
70
|
code_block=CodeBlockTools(sessions, backend),
|
|
68
71
|
imports=ImportTools(sessions, backend),
|
|
69
72
|
exports=ExportTools(sessions, backend),
|
|
@@ -8,7 +8,10 @@ from typing import Any, TextIO
|
|
|
8
8
|
def emit_text_result(result: dict[str, Any], *, hint: str, stream: TextIO) -> None:
|
|
9
9
|
text = _format_cancelled_result(result)
|
|
10
10
|
if text is None:
|
|
11
|
-
if
|
|
11
|
+
if hint in _DATA_FORMAT_HINTS:
|
|
12
|
+
# 数据面失败也必须保留 field_errors 与列表型 hints,不能先走通用失败裁剪。
|
|
13
|
+
text = _format_data_result(result)
|
|
14
|
+
elif _is_failed_result(result) and hint != "task_action_execute":
|
|
12
15
|
text = _format_failed_result(result)
|
|
13
16
|
else:
|
|
14
17
|
formatter = _FORMATTERS.get(hint, _format_generic)
|
|
@@ -90,6 +93,92 @@ def _format_generic(result: dict[str, Any]) -> str:
|
|
|
90
93
|
return "\n".join(lines) + "\n"
|
|
91
94
|
|
|
92
95
|
|
|
96
|
+
def _format_data_result(result: dict[str, Any]) -> str:
|
|
97
|
+
lines = [f"Status: {result.get('status') or ('success' if result.get('ok') is not False else 'failed')}"]
|
|
98
|
+
if result.get("format") == "csv":
|
|
99
|
+
files = result.get("files") if isinstance(result.get("files"), list) else []
|
|
100
|
+
lines.extend(
|
|
101
|
+
[
|
|
102
|
+
f"Local dir: {result.get('local_dir') or '-'}",
|
|
103
|
+
f"Files: {len(files)}",
|
|
104
|
+
]
|
|
105
|
+
)
|
|
106
|
+
for item in files:
|
|
107
|
+
if isinstance(item, dict):
|
|
108
|
+
lines.append(
|
|
109
|
+
f"- part {item.get('part')}: {item.get('local_path')} ({item.get('row_count')} rows)"
|
|
110
|
+
)
|
|
111
|
+
lines.extend(
|
|
112
|
+
[
|
|
113
|
+
f"Rows: {result.get('row_count')}",
|
|
114
|
+
f"Complete: {result.get('complete')}",
|
|
115
|
+
f"Truncated: {result.get('truncated')}",
|
|
116
|
+
f"Safe for final conclusion: {result.get('safe_for_final_conclusion')}",
|
|
117
|
+
f"Stopped reason: {result.get('stopped_reason') or '-'}",
|
|
118
|
+
]
|
|
119
|
+
)
|
|
120
|
+
access = result.get("access") if isinstance(result.get("access"), dict) else {}
|
|
121
|
+
if access:
|
|
122
|
+
sources = access.get("scope_sources") if isinstance(access.get("scope_sources"), list) else []
|
|
123
|
+
lines.append(
|
|
124
|
+
f"Access: level={access.get('level') or '-'} / can_write={access.get('can_write')}"
|
|
125
|
+
f" / scope_sources={','.join(str(item) for item in sources) or '-'}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
records = result.get("records") if isinstance(result.get("records"), dict) else {}
|
|
129
|
+
if records:
|
|
130
|
+
items = records.get("items") if isinstance(records.get("items"), list) else []
|
|
131
|
+
lines.append(
|
|
132
|
+
"Pagination: "
|
|
133
|
+
f"page={records.get('page')} / requested_page_size={records.get('requested_page_size')} / "
|
|
134
|
+
f"page_size={records.get('page_size')} / total={records.get('total')} / "
|
|
135
|
+
f"has_more={records.get('has_more')} / returned={len(items)}"
|
|
136
|
+
)
|
|
137
|
+
record = result.get("record") if isinstance(result.get("record"), dict) else {}
|
|
138
|
+
if record:
|
|
139
|
+
lines.append(f"Record ID: {record.get('record_id') or '-'}")
|
|
140
|
+
items = result.get("items") if isinstance(result.get("items"), list) else []
|
|
141
|
+
if items:
|
|
142
|
+
lines.append(f"Items: {len(items)}")
|
|
143
|
+
|
|
144
|
+
for key, label in (
|
|
145
|
+
("error_code", "Error Code"),
|
|
146
|
+
("message", "Message"),
|
|
147
|
+
("write_executed", "Write Executed"),
|
|
148
|
+
("retry_safe", "Retry Safe"),
|
|
149
|
+
):
|
|
150
|
+
if key in result:
|
|
151
|
+
lines.append(f"{label}: {result.get(key)}")
|
|
152
|
+
|
|
153
|
+
field_errors = result.get("field_errors") if isinstance(result.get("field_errors"), list) else []
|
|
154
|
+
if field_errors:
|
|
155
|
+
lines.append("Field Errors:")
|
|
156
|
+
_append_data_field_errors(lines, field_errors)
|
|
157
|
+
for index, item in enumerate(items):
|
|
158
|
+
if not isinstance(item, dict) or not isinstance(item.get("field_errors"), list):
|
|
159
|
+
continue
|
|
160
|
+
lines.append(f"Item {index} Field Errors:")
|
|
161
|
+
_append_data_field_errors(lines, item["field_errors"])
|
|
162
|
+
|
|
163
|
+
hints = result.get("hints") if isinstance(result.get("hints"), list) else []
|
|
164
|
+
lines.append(f"Hints: {len(hints)}")
|
|
165
|
+
for hint in hints:
|
|
166
|
+
if isinstance(hint, dict):
|
|
167
|
+
lines.append(f"- {hint.get('code') or '-'}: {hint.get('message') or '-'}")
|
|
168
|
+
else:
|
|
169
|
+
lines.append(f"- {hint}")
|
|
170
|
+
return "\n".join(lines) + "\n"
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _append_data_field_errors(lines: list[str], field_errors: list[Any]) -> None:
|
|
174
|
+
for error in field_errors:
|
|
175
|
+
if isinstance(error, dict):
|
|
176
|
+
lines.append(
|
|
177
|
+
f"- {error.get('path') or '-'}: {error.get('message') or '-'}"
|
|
178
|
+
f" / fix_hint={error.get('fix_hint') or '-'}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
|
|
93
182
|
def _format_whoami(result: dict[str, Any]) -> str:
|
|
94
183
|
lines = [
|
|
95
184
|
f"User: {result.get('nick_name') or '-'} ({result.get('email') or '-'})",
|
|
@@ -1669,4 +1758,11 @@ _FORMATTERS = {
|
|
|
1669
1758
|
"builder_summary": _format_builder_summary,
|
|
1670
1759
|
"file_upload_local": _format_file_upload_local,
|
|
1671
1760
|
"feedback_submit": _format_feedback_submit,
|
|
1761
|
+
"data_query": _format_data_result,
|
|
1762
|
+
"data_get": _format_data_result,
|
|
1763
|
+
"data_insert": _format_data_result,
|
|
1764
|
+
"data_update": _format_data_result,
|
|
1765
|
+
"data_delete": _format_data_result,
|
|
1672
1766
|
}
|
|
1767
|
+
|
|
1768
|
+
_DATA_FORMAT_HINTS = {"data_query", "data_get", "data_insert", "data_update", "data_delete"}
|
|
@@ -40,6 +40,11 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
40
40
|
PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
|
|
41
41
|
PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
|
|
42
42
|
PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
|
|
43
|
+
PublicToolSpec(USER_DOMAIN, "data_query", ("data_query",), ("data", "query"), cli_show_effective_context=True),
|
|
44
|
+
PublicToolSpec(USER_DOMAIN, "data_get", ("data_get",), ("data", "get"), cli_show_effective_context=True),
|
|
45
|
+
PublicToolSpec(USER_DOMAIN, "data_insert", ("data_insert",), ("data", "insert"), cli_show_effective_context=True, cli_context_write=True),
|
|
46
|
+
PublicToolSpec(USER_DOMAIN, "data_update", ("data_update",), ("data", "update"), cli_show_effective_context=True, cli_context_write=True),
|
|
47
|
+
PublicToolSpec(USER_DOMAIN, "data_delete", ("data_delete",), ("data", "delete"), cli_show_effective_context=True, cli_context_write=True),
|
|
43
48
|
PublicToolSpec(USER_DOMAIN, "portal_list", ("portal_list",), ("portal", "list"), cli_show_effective_context=True),
|
|
44
49
|
PublicToolSpec(USER_DOMAIN, "portal_get", ("portal_get",), ("portal", "get"), cli_show_effective_context=True),
|
|
45
50
|
PublicToolSpec(USER_DOMAIN, "view_get", ("view_get",), ("view", "get"), cli_show_effective_context=True),
|
|
@@ -173,6 +178,9 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
|
|
|
173
178
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_list_versions", ("app_flow_list_versions",), ("builder", "flow", "versions"), has_contract=True, cli_show_effective_context=True),
|
|
174
179
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_restore_version", ("app_flow_restore_version",), ("builder", "flow", "restore"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
175
180
|
PublicToolSpec(BUILDER_DOMAIN, "app_flow_apply", ("app_flow_apply",), ("builder", "flow", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
181
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_views_schema", ("app_views_schema",), ("builder", "views", "schema")),
|
|
182
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_views_get", ("app_views_get",), ("builder", "views", "get"), cli_show_effective_context=True),
|
|
183
|
+
PublicToolSpec(BUILDER_DOMAIN, "app_views_validate", (), ("builder", "views", "validate"), mcp_public=True),
|
|
176
184
|
PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
177
185
|
PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
178
186
|
PublicToolSpec(BUILDER_DOMAIN, "portal_apply", ("portal_apply",), ("builder", "portal", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
|
|
@@ -51,9 +51,15 @@ COMMON_ERROR_DROP_TOP = {
|
|
|
51
51
|
|
|
52
52
|
SUCCESS_POLICY_BY_TOOL: dict[str, TransformFn] = {}
|
|
53
53
|
|
|
54
|
-
#:
|
|
54
|
+
#: 这些校验命令的产出契约明确包含 ok 字段(供阶段 2 SKILL.md 引用),
|
|
55
55
|
#: 需在 success 裁剪时保留 ok(默认 COMMON_SUCCESS_DROP_TOP 会丢弃 ok)。
|
|
56
|
-
|
|
56
|
+
#: `builder:app_views_validate`(views 声明式校验)同属校验类工具,ok 同样进产出契约。
|
|
57
|
+
_KEEP_OK_TOOLS = {
|
|
58
|
+
"builder:app_form_validate_formula",
|
|
59
|
+
"builder:app_form_validate_code_block",
|
|
60
|
+
"builder:app_flow_validate",
|
|
61
|
+
"builder:app_views_validate",
|
|
62
|
+
}
|
|
57
63
|
|
|
58
64
|
|
|
59
65
|
def trim_public_response(tool_name: str | None, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -1310,6 +1316,9 @@ _register_policy(
|
|
|
1310
1316
|
"app_flow_validate",
|
|
1311
1317
|
"app_flow_list_versions",
|
|
1312
1318
|
"app_flow_restore_version",
|
|
1319
|
+
"app_views_schema",
|
|
1320
|
+
"app_views_get",
|
|
1321
|
+
"app_views_validate",
|
|
1313
1322
|
"app_get_charts",
|
|
1314
1323
|
"app_get_buttons",
|
|
1315
1324
|
"app_get_associated_resources",
|
|
@@ -7,6 +7,7 @@ from mcp.server.fastmcp import FastMCP
|
|
|
7
7
|
from .backend_client import BackendClient
|
|
8
8
|
from .session_store import SessionStore
|
|
9
9
|
from .tools.app_tools import AppTools
|
|
10
|
+
from .tools.data_tools import DataTools
|
|
10
11
|
from .tools.auth_tools import AuthTools
|
|
11
12
|
from .tools.code_block_tools import CodeBlockTools
|
|
12
13
|
from .tools.feedback_tools import FeedbackTools
|
|
@@ -249,6 +250,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
249
250
|
TaskContextTools(sessions, backend).register(server)
|
|
250
251
|
RoleTools(sessions, backend).register(server)
|
|
251
252
|
AppTools(sessions, backend).register(server)
|
|
253
|
+
DataTools(sessions, backend).register(server)
|
|
252
254
|
QingbiReportTools(sessions, backend).register(server)
|
|
253
255
|
PackageTools(sessions, backend).register(server)
|
|
254
256
|
NavigationTools(sessions, backend).register(server)
|
|
@@ -56,8 +56,8 @@ def build_builder_server() -> FastMCP:
|
|
|
56
56
|
"For builder delete/remove apply results, separate delete execution from readback verification: after DELETE is sent, resources expose delete_executed, readback_status, and safe_to_retry_delete=false. If readback_status is unavailable or still_exists, do not blindly repeat the delete; confirm later with app_get/view_get/chart_get or the relevant apply readback. Views/buttons use single-item readback; associated resources use one app-level resource-pool readback because there is no confirmed single-item GET. "
|
|
57
57
|
"For builder write/apply results, separate write execution from final readback verification: status=partial_success with write_executed=true and safe_to_retry=false means the write was sent or succeeded, while final readback, publish verification, or metadata confirmation is pending or permission-restricted. Report it as written but unverified; do not repeat the same write blindly. "
|
|
58
58
|
"For app_form_apply, pin the version from app_form_schema, preserve the complete body and existing IDs from a draft get, and configure data display through app-level spec.dataTitleField/spec.dataCoverField selectors rather than booleans on fields; App Builder MCP validates the declaration inside app_form_apply, while the CLI route has a separate static validate command. dataTitleField must resolve to exactly one top-level field for a non-empty body, while dataCoverField is optional and must resolve to a top-level attachment field. A root appKey means update and may include packageId; no appKey means create and requires packageId plus spec.appName. AppForm does not accept spec.icon or spec.color; creation derives a stable default icon and color from spec.appName. "
|
|
59
|
-
"For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows
|
|
60
|
-
"For ordinary business buttons, declare them on
|
|
59
|
+
"For app_views_apply, keep fixed saved filters in filters and configure the frontend query panel separately with query_conditions; query_conditions.rows accepts field names, field IDs, or queIds and compiles them to backend queryCondition queIds, with queIds preferred. New views default associated report/view display to visible with limit_type=all; existing views preserve their current associated display unless associated_resources is explicitly patched. View writes use ViewManagementAuth (beingViewManageStatus), falling back to DataManageAuth when advanced app permissions are not enabled. "
|
|
60
|
+
"For ordinary business buttons, declare them on a new or full view with app_views_apply.upsert_view.action_buttons; app_views_apply writes the view first, then creates/binds the button. For an existing view, use patch_views[].set.action_buttons. Use app_custom_buttons_apply only when the user explicitly asks to maintain existing standalone buttons outside view design, such as custom style/icon changes, cross-view reuse, deleting button bodies, bulk binding reorder, or exact qRobot/wings config. Button body writes require EditAppAuth; view_configs placement writes require ViewManagementAuth, matching the backend viewConfig route. For addData buttons, prefer action_buttons.target_app_key + field_mappings/default_values on the view path; do not ask agents to write raw que_relation unless maintaining a legacy config. field_mappings.source_field accepts source schema fields and supported system fields: 数据ID/row_record_id/apply_id/_id means current record id (-17), 编号/record_number means visible record number (0). To fill a target relation with the current record, map {'source_field': '数据ID', 'target_field': '目标引用字段'}; default_values is only for static constants. View button bindings merge by default; standalone view_configs are only for explicit maintenance. Builder view_key arguments are raw keys from app_get.views[].view_key and must not be prefixed with custom:. "
|
|
61
61
|
"For BI reports, keep report-body development separate from Qingflow in-app display: use app_charts_apply to create, update, remove, or reorder app-source QingBI chart bodies/configs with dataSourceType=qingflow; chart dimension/metric/filter/query fields must come from app_get_fields.chart_fields, not record schema or form-only fields; dataset BI reports are not created or edited by app_charts_apply yet and should be created in QingBI first, then attached with app_associated_resources_apply using report_source=dataset. "
|
|
62
62
|
"For associated views/reports, use app_associated_resources_apply. Use match_mappings for filtering associated resources: dynamic current-record conditions use source_field, static conditions use value. match_mappings also supports 数据ID(-17) and 编号(0). Do not ask agents to write raw match_rules unless preserving a legacy backend config. "
|
|
63
63
|
"For associated reports/views, use app_associated_resources_apply for both the app-level associated_resources pool and per-view display config; associated_item_id is the app-level form_asos_chart.id, and view_configs/remove/reorder may also pass an existing resource's chart_id/chart_key/view_key because the tool resolves those to the internal id. Before creating an associated resource, read app_get.associated_resources and reuse an existing matching target_app_key + view_key/chart_key through patch_resources; client_key only works inside one apply call and is not persisted. Do not ask agents to pass backend raw sourceType: views infer the internal Qingflow view source, reports default to BI app reports, and dataset reports use report_source=dataset. "
|
|
@@ -231,6 +231,32 @@ def build_builder_server() -> FastMCP:
|
|
|
231
231
|
) -> dict:
|
|
232
232
|
return ai_builder.app_form_apply(profile=profile, spec=spec or {})
|
|
233
233
|
|
|
234
|
+
@server.tool()
|
|
235
|
+
def app_views_schema(
|
|
236
|
+
schema_version: str | None = None,
|
|
237
|
+
schema_kind: str | None = None,
|
|
238
|
+
) -> dict:
|
|
239
|
+
"""Get a complete views schema or a focused detail schema such as display or button-config."""
|
|
240
|
+
return ai_builder.app_views_schema(schema_version=schema_version, schema_kind=schema_kind)
|
|
241
|
+
|
|
242
|
+
@server.tool()
|
|
243
|
+
def app_views_get(
|
|
244
|
+
profile: str = DEFAULT_PROFILE,
|
|
245
|
+
app_key: str = "",
|
|
246
|
+
schema_version: str | None = None,
|
|
247
|
+
schema_kind: str | None = None,
|
|
248
|
+
) -> dict:
|
|
249
|
+
return ai_builder.app_views_get(
|
|
250
|
+
profile=profile, app_key=app_key, schema_version=schema_version, schema_kind=schema_kind
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@server.tool()
|
|
254
|
+
def app_views_validate(
|
|
255
|
+
declaration: dict | None = None,
|
|
256
|
+
schema_version: str | None = None,
|
|
257
|
+
) -> dict:
|
|
258
|
+
return ai_builder.app_views_validate(declaration=declaration or {}, schema_version=schema_version)
|
|
259
|
+
|
|
234
260
|
@server.tool(description=ai_builder._high_risk_tool_description(operation="delete", target="app configuration"))
|
|
235
261
|
def app_delete(
|
|
236
262
|
profile: str = DEFAULT_PROFILE,
|
|
@@ -445,8 +471,8 @@ def build_builder_server() -> FastMCP:
|
|
|
445
471
|
return ai_builder.app_get_charts(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
446
472
|
|
|
447
473
|
@server.tool()
|
|
448
|
-
def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
|
|
449
|
-
return ai_builder.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys)
|
|
474
|
+
def app_get_buttons(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None, button_text: str | None = None) -> dict:
|
|
475
|
+
return ai_builder.app_get_buttons(profile=profile, app_key=app_key, app_keys=app_keys, button_text=button_text)
|
|
450
476
|
|
|
451
477
|
@server.tool()
|
|
452
478
|
def app_get_associated_resources(profile: str = DEFAULT_PROFILE, app_key: str = "", app_keys: list[str] | None = None) -> dict:
|
|
@@ -546,21 +572,19 @@ def build_builder_server() -> FastMCP:
|
|
|
546
572
|
profile: str = DEFAULT_PROFILE,
|
|
547
573
|
app_key: str = "",
|
|
548
574
|
publish: bool = True,
|
|
549
|
-
|
|
575
|
+
upsert_view: dict | None = None,
|
|
550
576
|
patch_views: list[dict] | None = None,
|
|
551
577
|
remove_views: list[str] | None = None,
|
|
552
|
-
|
|
553
|
-
views: list[dict] | None = None,
|
|
578
|
+
schema_version: str | None = None,
|
|
554
579
|
) -> dict:
|
|
555
580
|
return ai_builder.app_views_apply(
|
|
556
581
|
profile=profile,
|
|
557
582
|
app_key=app_key,
|
|
558
583
|
publish=publish,
|
|
559
|
-
|
|
584
|
+
upsert_view=upsert_view,
|
|
560
585
|
patch_views=patch_views or [],
|
|
561
586
|
remove_views=remove_views or [],
|
|
562
|
-
|
|
563
|
-
views=views,
|
|
587
|
+
schema_version=schema_version,
|
|
564
588
|
)
|
|
565
589
|
|
|
566
590
|
@server.tool()
|
|
@@ -10,6 +10,7 @@ from .config import DEFAULT_PROFILE
|
|
|
10
10
|
from .response_trim import USER_SERVER_METHOD_MAP, trim_error_response, trim_public_response, wrap_trimmed_methods
|
|
11
11
|
from .session_store import SessionStore
|
|
12
12
|
from .tools.app_tools import AppTools
|
|
13
|
+
from .tools.data_tools import DataTools
|
|
13
14
|
from .tools.auth_tools import AuthTools
|
|
14
15
|
from .tools.code_block_tools import CodeBlockTools
|
|
15
16
|
from .tools.directory_tools import DirectoryTools
|
|
@@ -242,6 +243,7 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
242
243
|
backend = BackendClient()
|
|
243
244
|
auth = wrap_trimmed_methods(AuthTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
244
245
|
apps = wrap_trimmed_methods(AppTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
246
|
+
data_tools = wrap_trimmed_methods(DataTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
245
247
|
workspace = wrap_trimmed_methods(WorkspaceTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
246
248
|
file_tools = wrap_trimmed_methods(FileTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
247
249
|
imports = wrap_trimmed_methods(ImportTools(sessions, backend), USER_SERVER_METHOD_MAP)
|
|
@@ -395,6 +397,8 @@ If the current MCP capability is unsupported, the workflow is awkward, or the us
|
|
|
395
397
|
def app_get(profile: str = DEFAULT_PROFILE, app_key: str = "") -> dict:
|
|
396
398
|
return apps.app_get(profile=profile, app_key=app_key)
|
|
397
399
|
|
|
400
|
+
data_tools.register(server)
|
|
401
|
+
|
|
398
402
|
@server.tool()
|
|
399
403
|
def portal_list(profile: str = DEFAULT_PROFILE) -> dict:
|
|
400
404
|
return resources.portal_list(profile=profile)
|
|
@@ -12,6 +12,7 @@ VIEW_TYPE_MAP = {
|
|
|
12
12
|
ViewType.board: "boardView",
|
|
13
13
|
ViewType.gantt: "ganttView",
|
|
14
14
|
ViewType.hierarchy: "hierarchyView",
|
|
15
|
+
ViewType.calendar: "calendarView",
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
|
|
@@ -32,7 +33,7 @@ def compile_views(entity: EntitySpec) -> list[dict[str, Any]]:
|
|
|
32
33
|
"viewgraphQueIds": list(view.field_ids),
|
|
33
34
|
"viewgraphQuestions": [],
|
|
34
35
|
"ordinal": ordinal,
|
|
35
|
-
"beingShowTitleQue": view.type in (ViewType.card, ViewType.board, ViewType.gantt, ViewType.hierarchy),
|
|
36
|
+
"beingShowTitleQue": view.type in (ViewType.card, ViewType.board, ViewType.gantt, ViewType.hierarchy, ViewType.calendar),
|
|
36
37
|
"beingShowCover": False,
|
|
37
38
|
"viewgraphLimitType": 1,
|
|
38
39
|
"viewgraphLimit": view.filters,
|