@qingflow-tech/qingflow-cli 1.1.47 → 1.1.48-beta.2
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 +1 -1
- package/skills/qingflow-cli/SKILL.md +91 -396
- 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_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/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/record/QINGFLOW_CLI_LEGACY_RECORD_RETRIEVAL_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 +13 -4
- package/src/qingflow_mcp/builder_facade/service.py +119 -1
- package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
- package/src/qingflow_mcp/cli/commands/auth.py +8 -0
- 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 +6 -0
- package/src/qingflow_mcp/cli/formatters.py +163 -3
- package/src/qingflow_mcp/cli/main.py +4 -1
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/public_surface.py +7 -0
- package/src/qingflow_mcp/resource_uri.py +241 -0
- package/src/qingflow_mcp/response_trim.py +10 -2
- package/src/qingflow_mcp/server.py +4 -0
- package/src/qingflow_mcp/server_app_builder.py +16 -0
- package/src/qingflow_mcp/server_app_user.py +24 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +208 -39
- package/src/qingflow_mcp/tools/app_tools.py +52 -13
- package/src/qingflow_mcp/tools/csv_export.py +168 -0
- package/src/qingflow_mcp/tools/data_tools.py +271 -0
- package/src/qingflow_mcp/tools/login_link_tools.py +142 -0
- package/src/qingflow_mcp/tools/record_tools.py +11 -46
- package/src/qingflow_mcp/tools/resource_read_tools.py +13 -0
|
@@ -19,6 +19,7 @@ from ..backend_client import BackendRequestContext
|
|
|
19
19
|
from ..errors import QingflowApiError, backend_code_int, backend_code_value_int, is_auth_like_error
|
|
20
20
|
from ..json_types import JSONObject
|
|
21
21
|
from ..list_type_labels import RECORD_LIST_TYPE_LABELS, SYSTEM_VIEW_ID_TO_LIST_TYPE
|
|
22
|
+
from ..resource_uri import validate_positive_resource_id, validate_resource_identity
|
|
22
23
|
from ..solution.build_assembly_store import BuildAssemblyStore, default_artifacts, default_manifest
|
|
23
24
|
from ..solution.compiler.chart_compiler import qingbi_workspace_visible_auth
|
|
24
25
|
from ..solution.compiler.form_compiler import build_question, default_form_payload, default_member_auth
|
|
@@ -1491,6 +1492,74 @@ class AiBuilderFacade:
|
|
|
1491
1492
|
details={"solution_key": requested_solution_key},
|
|
1492
1493
|
suggested_next_call={"tool_name": "solution_install", "arguments": {"profile": profile, **normalized_args}},
|
|
1493
1494
|
)
|
|
1495
|
+
raw_result = created.get("result") if isinstance(created.get("result"), dict) else {}
|
|
1496
|
+
raw_tag_ids = raw_result.get("tagIds")
|
|
1497
|
+
raw_app_keys = raw_result.get("appKeys")
|
|
1498
|
+
tag_ids, tag_ids_complete = _unique_resource_tag_ids(raw_tag_ids)
|
|
1499
|
+
app_keys, app_keys_complete = _unique_resource_app_keys(raw_app_keys)
|
|
1500
|
+
resource_list_complete = tag_ids_complete and app_keys_complete
|
|
1501
|
+
resources: list[JSONObject] = []
|
|
1502
|
+
warnings: list[JSONObject] = []
|
|
1503
|
+
|
|
1504
|
+
for tag_id in tag_ids:
|
|
1505
|
+
name = None
|
|
1506
|
+
try:
|
|
1507
|
+
detail = self.packages.package_get(profile=profile, tag_id=tag_id, include_raw=False)
|
|
1508
|
+
package = detail.get("result") if isinstance(detail.get("result"), dict) else {}
|
|
1509
|
+
name = str(package.get("tagName") or package.get("tag_name") or "").strip() or None
|
|
1510
|
+
except (QingflowApiError, RuntimeError):
|
|
1511
|
+
pass
|
|
1512
|
+
if not name:
|
|
1513
|
+
warnings.append(_warning("RESOURCE_NAME_UNAVAILABLE", "installed package name could not be read", package_id=tag_id))
|
|
1514
|
+
resources.append(
|
|
1515
|
+
_compact_dict(
|
|
1516
|
+
{
|
|
1517
|
+
"resource_type": "package",
|
|
1518
|
+
"operation": "installed",
|
|
1519
|
+
"status": "success",
|
|
1520
|
+
"id": tag_id,
|
|
1521
|
+
"key": str(tag_id),
|
|
1522
|
+
"name": name,
|
|
1523
|
+
"ids": {"package_id": tag_id},
|
|
1524
|
+
}
|
|
1525
|
+
)
|
|
1526
|
+
)
|
|
1527
|
+
|
|
1528
|
+
for app_key in app_keys:
|
|
1529
|
+
name = None
|
|
1530
|
+
parent = None
|
|
1531
|
+
try:
|
|
1532
|
+
detail = self.apps.app_get_base(profile=profile, app_key=app_key, include_raw=False)
|
|
1533
|
+
app = detail.get("result") if isinstance(detail.get("result"), dict) else {}
|
|
1534
|
+
name = str(app.get("formTitle") or app.get("appName") or "").strip() or None
|
|
1535
|
+
matching_tag_ids = [tag_id for tag_id in _unique_positive_ints(app.get("tagIds")) if tag_id in tag_ids]
|
|
1536
|
+
if len(matching_tag_ids) == 1:
|
|
1537
|
+
parent = {"resource_type": "package", "id": matching_tag_ids[0], "key": str(matching_tag_ids[0])}
|
|
1538
|
+
except (QingflowApiError, RuntimeError):
|
|
1539
|
+
pass
|
|
1540
|
+
if not name:
|
|
1541
|
+
warnings.append(_warning("RESOURCE_NAME_UNAVAILABLE", "installed application name could not be read", app_key=app_key))
|
|
1542
|
+
resources.append(
|
|
1543
|
+
_compact_dict(
|
|
1544
|
+
{
|
|
1545
|
+
"resource_type": "app",
|
|
1546
|
+
"operation": "installed",
|
|
1547
|
+
"status": "success",
|
|
1548
|
+
"key": app_key,
|
|
1549
|
+
"name": name,
|
|
1550
|
+
"ids": {"app_key": app_key},
|
|
1551
|
+
"parent": parent,
|
|
1552
|
+
}
|
|
1553
|
+
)
|
|
1554
|
+
)
|
|
1555
|
+
|
|
1556
|
+
if not resource_list_complete:
|
|
1557
|
+
warnings.append(
|
|
1558
|
+
_warning(
|
|
1559
|
+
"RESOURCE_LIST_INCOMPLETE",
|
|
1560
|
+
"solution installation succeeded, but its returned package/application identity list was incomplete",
|
|
1561
|
+
)
|
|
1562
|
+
)
|
|
1494
1563
|
return {
|
|
1495
1564
|
"status": "success",
|
|
1496
1565
|
"error_code": None,
|
|
@@ -1503,9 +1572,15 @@ class AiBuilderFacade:
|
|
|
1503
1572
|
"request_id": created.get("request_id"),
|
|
1504
1573
|
"suggested_next_call": None,
|
|
1505
1574
|
"noop": False,
|
|
1506
|
-
"
|
|
1575
|
+
"warnings": warnings,
|
|
1576
|
+
"verified": False,
|
|
1577
|
+
"verification": {
|
|
1578
|
+
"resource_list_complete": resource_list_complete,
|
|
1579
|
+
"business_verified": False,
|
|
1580
|
+
},
|
|
1507
1581
|
"write_executed": True,
|
|
1508
1582
|
"safe_to_retry": False,
|
|
1583
|
+
"resources": resources,
|
|
1509
1584
|
}
|
|
1510
1585
|
|
|
1511
1586
|
def package_list(self, *, profile: str, trial_status: str = "all", query: str = "") -> JSONObject:
|
|
@@ -17148,6 +17223,49 @@ def _coerce_positive_int(value: Any) -> int | None:
|
|
|
17148
17223
|
return None
|
|
17149
17224
|
|
|
17150
17225
|
|
|
17226
|
+
def _unique_positive_ints(value: Any) -> list[int]:
|
|
17227
|
+
if not isinstance(value, list):
|
|
17228
|
+
return []
|
|
17229
|
+
result: list[int] = []
|
|
17230
|
+
for item in value:
|
|
17231
|
+
number = _coerce_positive_int(item)
|
|
17232
|
+
if number is not None and number not in result:
|
|
17233
|
+
result.append(number)
|
|
17234
|
+
return result
|
|
17235
|
+
|
|
17236
|
+
|
|
17237
|
+
def _unique_resource_tag_ids(value: Any) -> tuple[list[int], bool]:
|
|
17238
|
+
if not isinstance(value, list):
|
|
17239
|
+
return [], False
|
|
17240
|
+
result: list[int] = []
|
|
17241
|
+
complete = True
|
|
17242
|
+
for item in value:
|
|
17243
|
+
try:
|
|
17244
|
+
tag_id = validate_positive_resource_id(item, "package_id")
|
|
17245
|
+
except (ValueError, UnicodeError):
|
|
17246
|
+
complete = False
|
|
17247
|
+
continue
|
|
17248
|
+
if tag_id not in result:
|
|
17249
|
+
result.append(tag_id)
|
|
17250
|
+
return result, complete
|
|
17251
|
+
|
|
17252
|
+
|
|
17253
|
+
def _unique_resource_app_keys(value: Any) -> tuple[list[str], bool]:
|
|
17254
|
+
if not isinstance(value, list):
|
|
17255
|
+
return [], False
|
|
17256
|
+
result: list[str] = []
|
|
17257
|
+
complete = True
|
|
17258
|
+
for item in value:
|
|
17259
|
+
try:
|
|
17260
|
+
app_key = validate_resource_identity(item, "app_key")
|
|
17261
|
+
except (ValueError, UnicodeError):
|
|
17262
|
+
complete = False
|
|
17263
|
+
continue
|
|
17264
|
+
if app_key not in result:
|
|
17265
|
+
result.append(app_key)
|
|
17266
|
+
return result, complete
|
|
17267
|
+
|
|
17268
|
+
|
|
17151
17269
|
def _coerce_nonnegative_int(value: Any) -> int | None:
|
|
17152
17270
|
if isinstance(value, bool) or value is None:
|
|
17153
17271
|
return None
|
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
|
|
5
|
-
from . import app, auth, builder, chart, exports, imports, portal, record, task, view, workspace
|
|
5
|
+
from . import app, auth, builder, chart, data, exports, imports, portal, record, task, view, workspace
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
def register_all_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -13,6 +13,7 @@ def register_all_commands(subparsers: argparse._SubParsersAction[argparse.Argume
|
|
|
13
13
|
view.register(subparsers)
|
|
14
14
|
chart.register(subparsers)
|
|
15
15
|
record.register(subparsers)
|
|
16
|
+
data.register(subparsers)
|
|
16
17
|
imports.register(subparsers)
|
|
17
18
|
exports.register(subparsers)
|
|
18
19
|
task.register(subparsers)
|
|
@@ -40,11 +40,19 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
40
40
|
whoami = auth_subparsers.add_parser("whoami", help="查看当前登录态")
|
|
41
41
|
whoami.set_defaults(handler=_handle_whoami, format_hint="auth_whoami")
|
|
42
42
|
|
|
43
|
+
login_link = auth_subparsers.add_parser("login-link", help="生成当前工作区的临时一次性登录链接(不自动打开)")
|
|
44
|
+
login_link.add_argument("--redirect-path", default="", help="登录后跳转的站内路径;资源路径必须属于当前工作区,其他业务路径可包含查询参数和锚点")
|
|
45
|
+
login_link.set_defaults(handler=_handle_login_link, format_hint="login_link")
|
|
46
|
+
|
|
43
47
|
logout = auth_subparsers.add_parser("logout", help="退出登录")
|
|
44
48
|
logout.add_argument("--forget-persisted", action="store_true")
|
|
45
49
|
logout.set_defaults(handler=_handle_logout, format_hint="")
|
|
46
50
|
|
|
47
51
|
|
|
52
|
+
def _handle_login_link(args: argparse.Namespace, context: CliContext) -> dict:
|
|
53
|
+
return context.login_link.get_login_link(profile=args.profile, redirect_path=args.redirect_path)
|
|
54
|
+
|
|
55
|
+
|
|
48
56
|
def _handle_login(args: argparse.Namespace, context: CliContext) -> dict:
|
|
49
57
|
if args.method == "oauth":
|
|
50
58
|
return _handle_oauth_login(args, context)
|
|
@@ -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,10 +11,12 @@ 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
|
|
17
18
|
from ..tools.import_tools import ImportTools
|
|
19
|
+
from ..tools.login_link_tools import LoginLinkTools
|
|
18
20
|
from ..tools.record_tools import RecordTools
|
|
19
21
|
from ..tools.resource_read_tools import ResourceReadTools
|
|
20
22
|
from ..tools.task_context_tools import TaskContextTools
|
|
@@ -27,10 +29,12 @@ class CliContext:
|
|
|
27
29
|
sessions: CliSessionFacade
|
|
28
30
|
backend: BackendClient
|
|
29
31
|
auth: AuthTools
|
|
32
|
+
login_link: LoginLinkTools
|
|
30
33
|
workspace: WorkspaceTools
|
|
31
34
|
app: AppTools
|
|
32
35
|
resource: ResourceReadTools
|
|
33
36
|
record: RecordTools
|
|
37
|
+
data: DataTools
|
|
34
38
|
code_block: CodeBlockTools
|
|
35
39
|
imports: ImportTools
|
|
36
40
|
exports: ExportTools
|
|
@@ -57,10 +61,12 @@ def build_cli_context() -> CliContext:
|
|
|
57
61
|
sessions=sessions,
|
|
58
62
|
backend=backend,
|
|
59
63
|
auth=AuthTools(sessions, backend),
|
|
64
|
+
login_link=LoginLinkTools(sessions, backend),
|
|
60
65
|
workspace=WorkspaceTools(sessions, backend),
|
|
61
66
|
app=AppTools(sessions, backend),
|
|
62
67
|
resource=ResourceReadTools(sessions, backend),
|
|
63
68
|
record=RecordTools(sessions, backend),
|
|
69
|
+
data=DataTools(sessions, backend),
|
|
64
70
|
code_block=CodeBlockTools(sessions, backend),
|
|
65
71
|
imports=ImportTools(sessions, backend),
|
|
66
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 '-'})",
|
|
@@ -191,9 +280,10 @@ def _format_app_items(result: dict[str, Any]) -> str:
|
|
|
191
280
|
str(item.get("app_key") or item.get("appKey") or ""),
|
|
192
281
|
str(item.get("app_name") or item.get("name") or item.get("title") or ""),
|
|
193
282
|
str(item.get("package_name") or item.get("tag_name") or ""),
|
|
283
|
+
str(item.get("uri") or ""),
|
|
194
284
|
]
|
|
195
285
|
)
|
|
196
|
-
return _render_titled_table("Apps", ["app_key", "app_name", "package"], rows)
|
|
286
|
+
return _render_titled_table("Apps", ["app_key", "app_name", "package", "uri"], rows)
|
|
197
287
|
|
|
198
288
|
|
|
199
289
|
def _format_app_get(result: dict[str, Any]) -> str:
|
|
@@ -201,6 +291,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
201
291
|
lines = [
|
|
202
292
|
f"App: {data.get('app_name') or '-'}",
|
|
203
293
|
f"App Key: {data.get('app_key') or '-'}",
|
|
294
|
+
f"URI: {data.get('uri') or '-'}",
|
|
204
295
|
f"Can Create: {data.get('can_create')}",
|
|
205
296
|
]
|
|
206
297
|
import_capability = data.get("import_capability")
|
|
@@ -224,7 +315,7 @@ def _format_app_get(result: dict[str, Any]) -> str:
|
|
|
224
315
|
lines.append(f"Accessible Views: {len(views)}")
|
|
225
316
|
for item in views[:10]:
|
|
226
317
|
if isinstance(item, dict):
|
|
227
|
-
lines.append(f"- {item.get('view_id')}: {item.get('name')}")
|
|
318
|
+
lines.append(f"- {item.get('view_id')}: {item.get('name')} / {item.get('uri') or '-'}")
|
|
228
319
|
_append_warnings(lines, result.get("warnings"))
|
|
229
320
|
return "\n".join(lines) + "\n"
|
|
230
321
|
|
|
@@ -296,6 +387,7 @@ def _format_view_get(result: dict[str, Any]) -> str:
|
|
|
296
387
|
f"View Key: {data.get('view_key') or '-'}",
|
|
297
388
|
f"App Key: {data.get('app_key') or '-'}",
|
|
298
389
|
f"View Type: {data.get('view_type') or '-'}",
|
|
390
|
+
f"URI: {data.get('uri') or '-'}",
|
|
299
391
|
f"Analysis Supported: {data.get('analysis_supported')}",
|
|
300
392
|
]
|
|
301
393
|
if export_capability:
|
|
@@ -1184,6 +1276,7 @@ def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
|
1184
1276
|
("view_key", "View Key"),
|
|
1185
1277
|
("chart_id", "Chart ID"),
|
|
1186
1278
|
("package_id", "Package ID"),
|
|
1279
|
+
("uri", "URI"),
|
|
1187
1280
|
("verified", "Verified"),
|
|
1188
1281
|
("write_executed", "Write Executed"),
|
|
1189
1282
|
("delete_executed", "Delete Executed"),
|
|
@@ -1216,6 +1309,7 @@ def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
|
1216
1309
|
"associations",
|
|
1217
1310
|
"package_ids",
|
|
1218
1311
|
"app_keys",
|
|
1312
|
+
"resources",
|
|
1219
1313
|
):
|
|
1220
1314
|
value = result.get(key)
|
|
1221
1315
|
if not isinstance(value, list) and isinstance(data, dict):
|
|
@@ -1226,6 +1320,15 @@ def _format_builder_summary(result: dict[str, Any]) -> str:
|
|
|
1226
1320
|
lines.append("- " + _builder_item_summary(item))
|
|
1227
1321
|
if len(value) > 8:
|
|
1228
1322
|
lines.append(f"... {len(value) - 8} more")
|
|
1323
|
+
if key == "apps":
|
|
1324
|
+
uri_lines = [
|
|
1325
|
+
line
|
|
1326
|
+
for item in value
|
|
1327
|
+
for line in _builder_batch_item_uri_lines(item)
|
|
1328
|
+
]
|
|
1329
|
+
if uri_lines:
|
|
1330
|
+
lines.append("Resource URIs:")
|
|
1331
|
+
lines.extend(f"- {line}" for line in uri_lines)
|
|
1229
1332
|
components = result.get("components") if isinstance(result.get("components"), list) else []
|
|
1230
1333
|
if components:
|
|
1231
1334
|
lines.append("Component Refs:")
|
|
@@ -1294,6 +1397,7 @@ def _builder_item_summary(item: Any) -> str:
|
|
|
1294
1397
|
"uid",
|
|
1295
1398
|
"userId",
|
|
1296
1399
|
"email",
|
|
1400
|
+
"uri",
|
|
1297
1401
|
)
|
|
1298
1402
|
parts = [
|
|
1299
1403
|
f"{key}={item.get(key)}"
|
|
@@ -1305,6 +1409,50 @@ def _builder_item_summary(item: Any) -> str:
|
|
|
1305
1409
|
return " / ".join(parts) if parts else json.dumps(item, ensure_ascii=False)
|
|
1306
1410
|
|
|
1307
1411
|
|
|
1412
|
+
def _builder_batch_item_uri_lines(item: Any) -> list[str]:
|
|
1413
|
+
if not isinstance(item, dict):
|
|
1414
|
+
return []
|
|
1415
|
+
item_status = str(item.get("status") or "").lower()
|
|
1416
|
+
if item_status not in {"success", "partial_success"}:
|
|
1417
|
+
return []
|
|
1418
|
+
data = item.get("data") if isinstance(item.get("data"), dict) else item.get("result")
|
|
1419
|
+
if not isinstance(data, dict) or not _builder_uri_is_confirmed(data, fallback_status=item_status):
|
|
1420
|
+
return []
|
|
1421
|
+
lines: list[str] = []
|
|
1422
|
+
if data.get("uri"):
|
|
1423
|
+
lines.append(f"URI: {data['uri']}")
|
|
1424
|
+
views = data.get("views") if isinstance(data.get("views"), list) else []
|
|
1425
|
+
for view in views:
|
|
1426
|
+
if (
|
|
1427
|
+
isinstance(view, dict)
|
|
1428
|
+
and view.get("uri")
|
|
1429
|
+
and _builder_uri_is_confirmed(view, fallback_status=item_status)
|
|
1430
|
+
):
|
|
1431
|
+
lines.append(f"View URI: {view['uri']}")
|
|
1432
|
+
return lines
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
def _builder_uri_is_confirmed(value: dict[str, Any], *, fallback_status: str) -> bool:
|
|
1436
|
+
status = str(value.get("status") or fallback_status).lower()
|
|
1437
|
+
operation = str(value.get("operation") or "").lower()
|
|
1438
|
+
return (
|
|
1439
|
+
status in {"success", "partial_success", "unchanged", "verified"}
|
|
1440
|
+
and operation
|
|
1441
|
+
not in {
|
|
1442
|
+
"failed",
|
|
1443
|
+
"pending",
|
|
1444
|
+
"planned",
|
|
1445
|
+
"readback_pending",
|
|
1446
|
+
"pending_readback",
|
|
1447
|
+
"unknown",
|
|
1448
|
+
"remove",
|
|
1449
|
+
"removed",
|
|
1450
|
+
"delete",
|
|
1451
|
+
"deleted",
|
|
1452
|
+
}
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
|
|
1308
1456
|
def _format_file_upload_local(result: dict[str, Any]) -> str:
|
|
1309
1457
|
lines = [
|
|
1310
1458
|
f"Upload Kind: {result.get('upload_kind') or result.get('requested_upload_kind') or '-'}",
|
|
@@ -1566,7 +1714,12 @@ def _task_action_debug_lines(result: dict[str, Any]) -> list[str]:
|
|
|
1566
1714
|
return lines
|
|
1567
1715
|
|
|
1568
1716
|
|
|
1717
|
+
def _format_login_link(result: dict[str, Any]) -> str:
|
|
1718
|
+
return f"登录链接:{result['url']}\n过期时间(Unix 毫秒):{result['expires_at']}\n链接只能使用一次,请勿泄露;仅在用户要求打开时才打开。\n"
|
|
1719
|
+
|
|
1720
|
+
|
|
1569
1721
|
_FORMATTERS = {
|
|
1722
|
+
"login_link": _format_login_link,
|
|
1570
1723
|
"auth_whoami": _format_whoami,
|
|
1571
1724
|
"workspace_list": _format_workspace_list,
|
|
1572
1725
|
"workspace_get": _format_workspace_get,
|
|
@@ -1604,4 +1757,11 @@ _FORMATTERS = {
|
|
|
1604
1757
|
"builder_summary": _format_builder_summary,
|
|
1605
1758
|
"file_upload_local": _format_file_upload_local,
|
|
1606
1759
|
"feedback_submit": _format_feedback_submit,
|
|
1760
|
+
"data_query": _format_data_result,
|
|
1761
|
+
"data_get": _format_data_result,
|
|
1762
|
+
"data_insert": _format_data_result,
|
|
1763
|
+
"data_update": _format_data_result,
|
|
1764
|
+
"data_delete": _format_data_result,
|
|
1607
1765
|
}
|
|
1766
|
+
|
|
1767
|
+
_DATA_FORMAT_HINTS = {"data_query", "data_get", "data_insert", "data_update", "data_delete"}
|
|
@@ -2,11 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from typing import Any, Callable, TextIO
|
|
7
8
|
|
|
8
9
|
from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
|
|
9
10
|
from ..public_surface import cli_public_tool_spec_from_namespace
|
|
11
|
+
from ..resource_uri import cli_resource_base_url
|
|
10
12
|
from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
|
|
11
13
|
from ..tools.ai_builder_tools import _attach_builder_apply_envelope
|
|
12
14
|
from ..version import get_cli_version, get_cli_version_info
|
|
@@ -94,7 +96,8 @@ def run(
|
|
|
94
96
|
context = context_factory()
|
|
95
97
|
if not bool(args.json):
|
|
96
98
|
_emit_cli_effective_context_notice(args, context, stream=err)
|
|
97
|
-
|
|
99
|
+
with cli_resource_base_url(os.getenv("QINGFLOW_BASE_URL") or ""):
|
|
100
|
+
result = handler(args, context)
|
|
98
101
|
except SystemExit as exc:
|
|
99
102
|
return int(exc.code or 0)
|
|
100
103
|
except RuntimeError as exc:
|