@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.
Files changed (54) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +29 -13
  3. package/package.json +1 -1
  4. package/pyproject.toml +1 -1
  5. package/skills/qingflow-cli/SKILL.md +91 -396
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  16. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  17. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
  18. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  19. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  20. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  21. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  22. package/skills/qingflow-cli/reference/data/README.md +32 -0
  23. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_LEGACY_RECORD_RETRIEVAL_WORKFLOW.md +50 -0
  24. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  25. package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
  26. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  27. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  28. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  29. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  30. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
  31. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  32. package/skills/qingflow-common/SKILL.md +13 -4
  33. package/src/qingflow_mcp/builder_facade/service.py +119 -1
  34. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  35. package/src/qingflow_mcp/cli/commands/auth.py +8 -0
  36. package/src/qingflow_mcp/cli/commands/data.py +134 -0
  37. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  38. package/src/qingflow_mcp/cli/context.py +6 -0
  39. package/src/qingflow_mcp/cli/formatters.py +163 -3
  40. package/src/qingflow_mcp/cli/main.py +4 -1
  41. package/src/qingflow_mcp/config.py +39 -0
  42. package/src/qingflow_mcp/public_surface.py +7 -0
  43. package/src/qingflow_mcp/resource_uri.py +241 -0
  44. package/src/qingflow_mcp/response_trim.py +10 -2
  45. package/src/qingflow_mcp/server.py +4 -0
  46. package/src/qingflow_mcp/server_app_builder.py +16 -0
  47. package/src/qingflow_mcp/server_app_user.py +24 -0
  48. package/src/qingflow_mcp/tools/ai_builder_tools.py +208 -39
  49. package/src/qingflow_mcp/tools/app_tools.py +52 -13
  50. package/src/qingflow_mcp/tools/csv_export.py +168 -0
  51. package/src/qingflow_mcp/tools/data_tools.py +271 -0
  52. package/src/qingflow_mcp/tools/login_link_tools.py +142 -0
  53. package/src/qingflow_mcp/tools/record_tools.py +11 -46
  54. 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
- "verification": {},
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 _is_failed_result(result) and hint != "task_action_execute":
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
- result = handler(args, context)
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: