@josephyan/qingflow-cli 0.2.0-beta.72 → 0.2.0-beta.73

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 CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @josephyan/qingflow-cli@0.2.0-beta.72
6
+ npm install @josephyan/qingflow-cli@0.2.0-beta.73
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @josephyan/qingflow-cli@0.2.0-beta.72 qingflow
12
+ npx -y -p @josephyan/qingflow-cli@0.2.0-beta.73 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@josephyan/qingflow-cli",
3
- "version": "0.2.0-beta.72",
3
+ "version": "0.2.0-beta.73",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "0.2.0b72"
7
+ version = "0.2.0b73"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -187,7 +187,8 @@ class FlowConditionRulePatch(StrictModel):
187
187
  return value
188
188
  payload = dict(value)
189
189
  if "value" in payload and "values" not in payload:
190
- payload["values"] = [payload.pop("value")]
190
+ raw_value = payload.pop("value")
191
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
191
192
  raw_operator = payload.get("operator", payload.get("op"))
192
193
  if isinstance(raw_operator, str):
193
194
  normalized = raw_operator.strip().lower()
@@ -238,7 +239,8 @@ class ViewFilterRulePatch(StrictModel):
238
239
  return value
239
240
  payload = dict(value)
240
241
  if "value" in payload and "values" not in payload:
241
- payload["values"] = [payload.pop("value")]
242
+ raw_value = payload.pop("value")
243
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
242
244
  raw_operator = payload.get("operator", payload.get("op"))
243
245
  if isinstance(raw_operator, str):
244
246
  normalized = raw_operator.strip().lower()
@@ -1092,7 +1094,8 @@ class ChartFilterRulePatch(StrictModel):
1092
1094
  return value
1093
1095
  payload = dict(value)
1094
1096
  if "value" in payload and "values" not in payload:
1095
- payload["values"] = [payload.pop("value")]
1097
+ raw_value = payload.pop("value")
1098
+ payload["values"] = list(raw_value) if isinstance(raw_value, list) else [raw_value]
1096
1099
  raw_operator = payload.get("operator", payload.get("op"))
1097
1100
  if isinstance(raw_operator, str):
1098
1101
  normalized = raw_operator.strip().lower()
@@ -10,6 +10,34 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
10
10
  parser = subparsers.add_parser("builder", aliases=["build"], help="稳定 builder 命令")
11
11
  builder_subparsers = parser.add_subparsers(dest="builder_command", required=True)
12
12
 
13
+ file_parser = builder_subparsers.add_parser("file", help="builder 侧文件上传")
14
+ file_subparsers = file_parser.add_subparsers(dest="builder_file_command", required=True)
15
+ file_upload_local = file_subparsers.add_parser("upload-local", help="上传本地文件并返回附件值")
16
+ file_upload_local.add_argument("--upload-kind", default="attachment")
17
+ file_upload_local.add_argument("--file-path", required=True)
18
+ file_upload_local.add_argument("--upload-mark")
19
+ file_upload_local.add_argument("--content-type")
20
+ file_upload_local.add_argument("--bucket-type")
21
+ file_upload_local.add_argument("--path-id", type=int)
22
+ file_upload_local.add_argument("--file-related-url")
23
+ file_upload_local.set_defaults(handler=_handle_file_upload_local, format_hint="generic")
24
+
25
+ feedback = builder_subparsers.add_parser("feedback", help="builder 侧反馈提交")
26
+ feedback_subparsers = feedback.add_subparsers(dest="builder_feedback_command", required=True)
27
+ feedback_submit = feedback_subparsers.add_parser("submit", help="提交 builder 能力反馈")
28
+ feedback_submit.add_argument("--category", required=True)
29
+ feedback_submit.add_argument("--title", required=True)
30
+ feedback_submit.add_argument("--description", required=True)
31
+ feedback_submit.add_argument("--expected-behavior")
32
+ feedback_submit.add_argument("--actual-behavior")
33
+ feedback_submit.add_argument("--impact-scope")
34
+ feedback_submit.add_argument("--tool-name")
35
+ feedback_submit.add_argument("--app-key")
36
+ feedback_submit.add_argument("--record-id")
37
+ feedback_submit.add_argument("--workflow-node-id")
38
+ feedback_submit.add_argument("--note")
39
+ feedback_submit.set_defaults(handler=_handle_feedback_submit, format_hint="generic")
40
+
13
41
  contract = builder_subparsers.add_parser("contract", help="读取 builder tool 合约")
14
42
  contract.add_argument("--tool-name", required=True)
15
43
  contract.set_defaults(handler=_handle_contract, format_hint="builder_summary")
@@ -215,6 +243,35 @@ def _handle_package_list(args: argparse.Namespace, context: CliContext) -> dict:
215
243
  return context.builder.package_list(profile=args.profile, trial_status=args.trial_status)
216
244
 
217
245
 
246
+ def _handle_file_upload_local(args: argparse.Namespace, context: CliContext) -> dict:
247
+ return context.files.file_upload_local(
248
+ profile=args.profile,
249
+ upload_kind=args.upload_kind,
250
+ file_path=args.file_path,
251
+ upload_mark=args.upload_mark,
252
+ content_type=args.content_type,
253
+ bucket_type=args.bucket_type,
254
+ path_id=args.path_id,
255
+ file_related_url=args.file_related_url,
256
+ )
257
+
258
+
259
+ def _handle_feedback_submit(args: argparse.Namespace, context: CliContext) -> dict:
260
+ return context.builder_feedback.feedback_submit(
261
+ category=args.category,
262
+ title=args.title,
263
+ description=args.description,
264
+ expected_behavior=args.expected_behavior,
265
+ actual_behavior=args.actual_behavior,
266
+ impact_scope=args.impact_scope,
267
+ tool_name=args.tool_name,
268
+ app_key=args.app_key,
269
+ record_id=args.record_id,
270
+ workflow_node_id=args.workflow_node_id,
271
+ note=args.note,
272
+ )
273
+
274
+
218
275
  def _handle_contract(args: argparse.Namespace, context: CliContext) -> dict:
219
276
  return context.builder.builder_tool_contract(tool_name=args.tool_name)
220
277
 
@@ -8,6 +8,8 @@ from ..tools.ai_builder_tools import AiBuilderTools
8
8
  from ..tools.app_tools import AppTools
9
9
  from ..tools.auth_tools import AuthTools
10
10
  from ..tools.code_block_tools import CodeBlockTools
11
+ from ..tools.feedback_tools import FeedbackTools
12
+ from ..tools.file_tools import FileTools
11
13
  from ..tools.import_tools import ImportTools
12
14
  from ..tools.record_tools import RecordTools
13
15
  from ..tools.resource_read_tools import ResourceReadTools
@@ -27,6 +29,8 @@ class CliContext:
27
29
  code_block: CodeBlockTools
28
30
  imports: ImportTools
29
31
  task: TaskContextTools
32
+ files: FileTools
33
+ builder_feedback: FeedbackTools
30
34
  builder: AiBuilderTools
31
35
 
32
36
  def close(self) -> None:
@@ -47,5 +51,7 @@ def build_cli_context() -> CliContext:
47
51
  code_block=CodeBlockTools(sessions, backend),
48
52
  imports=ImportTools(sessions, backend),
49
53
  task=TaskContextTools(sessions, backend),
54
+ files=FileTools(sessions, backend),
55
+ builder_feedback=FeedbackTools(backend, mcp_side="App Builder MCP"),
50
56
  builder=AiBuilderTools(sessions, backend),
51
57
  )
@@ -170,6 +170,14 @@ def resolve_cli_tool_name(args: Any) -> str | None:
170
170
  if command not in {"builder", "build"}:
171
171
  return None
172
172
  builder_command = getattr(args, "builder_command", None)
173
+ if builder_command == "file":
174
+ return {
175
+ "upload-local": _tool_key(BUILDER_DOMAIN, "file_upload_local"),
176
+ }.get(getattr(args, "builder_file_command", None))
177
+ if builder_command == "feedback":
178
+ return {
179
+ "submit": _tool_key(BUILDER_DOMAIN, "feedback_submit"),
180
+ }.get(getattr(args, "builder_feedback_command", None))
173
181
  if builder_command == "contract":
174
182
  return _tool_key(BUILDER_DOMAIN, "builder_tool_contract")
175
183
  if builder_command == "member":
@@ -1917,6 +1917,50 @@ def _public_error_message(error_code: str, error: QingflowApiError) -> str:
1917
1917
 
1918
1918
 
1919
1919
  _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
1920
+ "file_upload_local": {
1921
+ "allowed_keys": ["upload_kind", "file_path", "upload_mark", "content_type", "bucket_type", "path_id", "file_related_url"],
1922
+ "aliases": {},
1923
+ "allowed_values": {"upload_kind": ["attachment", "login"]},
1924
+ "execution_notes": [
1925
+ "returns upload metadata plus attachment-ready values for later builder or user writes",
1926
+ "upload_kind=attachment may fall back to login upload info when the backend rejects attachment upload info",
1927
+ "upload_mark is recommended for attachment uploads because it affects the remote storage path",
1928
+ ],
1929
+ "minimal_example": {
1930
+ "profile": "default",
1931
+ "upload_kind": "attachment",
1932
+ "file_path": "/absolute/path/to/local-file.txt",
1933
+ "upload_mark": "APP_KEY",
1934
+ },
1935
+ },
1936
+ "feedback_submit": {
1937
+ "allowed_keys": [
1938
+ "category",
1939
+ "title",
1940
+ "description",
1941
+ "expected_behavior",
1942
+ "actual_behavior",
1943
+ "impact_scope",
1944
+ "tool_name",
1945
+ "app_key",
1946
+ "record_id",
1947
+ "workflow_node_id",
1948
+ "note",
1949
+ ],
1950
+ "aliases": {},
1951
+ "allowed_values": {"category": ["bug", "missing_capability", "awkward_workflow", "ux", "docs"]},
1952
+ "execution_notes": [
1953
+ "submit feedback only after explicit user confirmation",
1954
+ "use this when the current builder capability is unsupported or awkward after reasonable attempts",
1955
+ "requires feedback qsource configuration but does not require Qingflow login or workspace selection",
1956
+ ],
1957
+ "minimal_example": {
1958
+ "category": "missing_capability",
1959
+ "title": "builder chart data read gap",
1960
+ "description": "The current builder capability cannot satisfy the requested workflow.",
1961
+ "tool_name": "chart_get",
1962
+ },
1963
+ },
1920
1964
  "package_list": {
1921
1965
  "allowed_keys": ["trial_status"],
1922
1966
  "aliases": {"trialStatus": "trial_status"},
@@ -2019,6 +2063,22 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2019
2063
  "package_tag_id": 1001,
2020
2064
  },
2021
2065
  },
2066
+ "app_release_edit_lock_if_mine": {
2067
+ "allowed_keys": ["app_key", "lock_owner_email", "lock_owner_name"],
2068
+ "aliases": {},
2069
+ "allowed_values": {},
2070
+ "execution_notes": [
2071
+ "use this only after a builder write fails with APP_EDIT_LOCKED and the lock belongs to the current user",
2072
+ "supply the lock owner details from the failed write result for a safe release attempt",
2073
+ "after a successful release, retry the blocked builder write or publish verification call",
2074
+ ],
2075
+ "minimal_example": {
2076
+ "profile": "default",
2077
+ "app_key": "APP_KEY",
2078
+ "lock_owner_email": "user@example.com",
2079
+ "lock_owner_name": "当前用户",
2080
+ },
2081
+ },
2022
2082
  "app_custom_button_list": {
2023
2083
  "allowed_keys": ["app_key"],
2024
2084
  "aliases": {},
@@ -2475,6 +2535,9 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2475
2535
  "view.buttons.button_type": ["SYSTEM", "CUSTOM"],
2476
2536
  "view.buttons.config_type": ["TOP", "DETAIL"],
2477
2537
  },
2538
+ "execution_notes": [
2539
+ "for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
2540
+ ],
2478
2541
  "minimal_example": {
2479
2542
  "profile": "default",
2480
2543
  "app_key": "APP_KEY",
@@ -2534,6 +2597,7 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2534
2597
  "read back app_get_views after any failed or partial view apply",
2535
2598
  "view existence verification and saved-filter verification are separate; treat filters as unverified until verification.view_filters_verified is true",
2536
2599
  "buttons omitted preserves existing button config; buttons=[] clears all buttons; buttons=[...] replaces the full button config",
2600
+ "for multi-value operators such as in, pass values as a list; value may also be used as an alias when it already contains a list",
2537
2601
  ],
2538
2602
  "minimal_example": {
2539
2603
  "profile": "default",
@@ -2779,6 +2843,21 @@ _BUILDER_TOOL_CONTRACTS: dict[str, JSONObject] = {
2779
2843
  },
2780
2844
  },
2781
2845
  },
2846
+ "app_publish_verify": {
2847
+ "allowed_keys": ["app_key", "expected_package_tag_id"],
2848
+ "aliases": {},
2849
+ "allowed_values": {},
2850
+ "execution_notes": [
2851
+ "verifies that the current app draft has been published and is readable through the builder surface",
2852
+ "expected_package_tag_id is optional and adds an extra package consistency check",
2853
+ "when verification fails because of an edit lock owned by the current user, retry after app_release_edit_lock_if_mine",
2854
+ ],
2855
+ "minimal_example": {
2856
+ "profile": "default",
2857
+ "app_key": "APP_KEY",
2858
+ "expected_package_tag_id": 1001,
2859
+ },
2860
+ },
2782
2861
  }
2783
2862
 
2784
2863
  _PRIVATE_BUILDER_TOOL_CONTRACTS = {