@qingflow-tech/qingflow-app-user-mcp 1.0.10 → 1.0.12

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 (89) hide show
  1. package/README.md +9 -3
  2. package/docs/local-agent-install.md +54 -3
  3. package/entry_point.py +1 -1
  4. package/npm/bin/qingflow-skills.mjs +5 -0
  5. package/npm/lib/runtime.mjs +304 -13
  6. package/npm/scripts/postinstall.mjs +1 -5
  7. package/package.json +3 -2
  8. package/pyproject.toml +1 -1
  9. package/skills/qingflow-app-builder/SKILL.md +255 -0
  10. package/skills/qingflow-app-builder/agents/openai.yaml +4 -0
  11. package/skills/qingflow-app-builder/references/create-app.md +149 -0
  12. package/skills/qingflow-app-builder/references/environments.md +63 -0
  13. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  14. package/skills/qingflow-app-builder/references/gotchas.md +107 -0
  15. package/skills/qingflow-app-builder/references/match-rules.md +114 -0
  16. package/skills/qingflow-app-builder/references/public-surface-sync.md +75 -0
  17. package/skills/qingflow-app-builder/references/solution-playbooks.md +52 -0
  18. package/skills/qingflow-app-builder/references/tool-selection.md +99 -0
  19. package/skills/qingflow-app-builder/references/update-flow.md +158 -0
  20. package/skills/qingflow-app-builder/references/update-layout.md +68 -0
  21. package/skills/qingflow-app-builder/references/update-schema.md +72 -0
  22. package/skills/qingflow-app-builder/references/update-views.md +284 -0
  23. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  24. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  25. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  26. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  27. package/skills/qingflow-app-user/SKILL.md +12 -11
  28. package/skills/qingflow-app-user/references/data-gotchas.md +2 -2
  29. package/skills/qingflow-app-user/references/public-surface-sync.md +3 -3
  30. package/skills/qingflow-app-user/references/record-patterns.md +5 -5
  31. package/skills/qingflow-app-user/references/workflow-usage.md +4 -5
  32. package/skills/qingflow-mcp-setup/SKILL.md +113 -0
  33. package/skills/qingflow-mcp-setup/agents/openai.yaml +4 -0
  34. package/skills/qingflow-mcp-setup/references/claude-desktop.md +34 -0
  35. package/skills/qingflow-mcp-setup/references/environments.md +62 -0
  36. package/skills/qingflow-mcp-setup/references/generic-stdio.md +32 -0
  37. package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +38 -0
  38. package/skills/qingflow-record-analysis/SKILL.md +6 -7
  39. package/skills/qingflow-record-analysis/manifest.yaml +10 -0
  40. package/skills/qingflow-record-delete/SKILL.md +5 -3
  41. package/skills/qingflow-record-import/SKILL.md +6 -2
  42. package/skills/qingflow-record-insert/SKILL.md +48 -4
  43. package/skills/qingflow-record-insert/manifest.yaml +6 -0
  44. package/skills/qingflow-record-update/SKILL.md +36 -24
  45. package/skills/qingflow-task-ops/SKILL.md +25 -25
  46. package/skills/qingflow-task-ops/references/environments.md +0 -1
  47. package/skills/qingflow-task-ops/references/workflow-usage.md +4 -6
  48. package/src/qingflow_mcp/__main__.py +6 -2
  49. package/src/qingflow_mcp/builder_facade/models.py +41 -2
  50. package/src/qingflow_mcp/builder_facade/service.py +2743 -423
  51. package/src/qingflow_mcp/cli/commands/app.py +3 -16
  52. package/src/qingflow_mcp/cli/commands/builder.py +30 -4
  53. package/src/qingflow_mcp/cli/commands/exports.py +2 -2
  54. package/src/qingflow_mcp/cli/commands/imports.py +1 -1
  55. package/src/qingflow_mcp/cli/commands/record.py +54 -11
  56. package/src/qingflow_mcp/cli/context.py +0 -3
  57. package/src/qingflow_mcp/cli/formatters.py +238 -8
  58. package/src/qingflow_mcp/cli/main.py +47 -3
  59. package/src/qingflow_mcp/errors.py +43 -2
  60. package/src/qingflow_mcp/public_surface.py +24 -16
  61. package/src/qingflow_mcp/response_trim.py +119 -12
  62. package/src/qingflow_mcp/server.py +17 -14
  63. package/src/qingflow_mcp/server_app_builder.py +29 -7
  64. package/src/qingflow_mcp/server_app_user.py +23 -24
  65. package/src/qingflow_mcp/solution/compiler/icon_utils.py +294 -0
  66. package/src/qingflow_mcp/solution/executor.py +112 -15
  67. package/src/qingflow_mcp/tools/ai_builder_tools.py +497 -65
  68. package/src/qingflow_mcp/tools/app_tools.py +237 -51
  69. package/src/qingflow_mcp/tools/approval_tools.py +196 -34
  70. package/src/qingflow_mcp/tools/auth_tools.py +92 -16
  71. package/src/qingflow_mcp/tools/code_block_tools.py +296 -39
  72. package/src/qingflow_mcp/tools/custom_button_tools.py +64 -10
  73. package/src/qingflow_mcp/tools/directory_tools.py +236 -72
  74. package/src/qingflow_mcp/tools/export_tools.py +230 -33
  75. package/src/qingflow_mcp/tools/file_tools.py +7 -3
  76. package/src/qingflow_mcp/tools/import_tools.py +293 -40
  77. package/src/qingflow_mcp/tools/navigation_tools.py +91 -12
  78. package/src/qingflow_mcp/tools/package_tools.py +134 -8
  79. package/src/qingflow_mcp/tools/portal_tools.py +39 -3
  80. package/src/qingflow_mcp/tools/qingbi_report_tools.py +116 -7
  81. package/src/qingflow_mcp/tools/record_tools.py +2305 -442
  82. package/src/qingflow_mcp/tools/resource_read_tools.py +191 -39
  83. package/src/qingflow_mcp/tools/role_tools.py +80 -9
  84. package/src/qingflow_mcp/tools/solution_tools.py +57 -15
  85. package/src/qingflow_mcp/tools/task_context_tools.py +569 -119
  86. package/src/qingflow_mcp/tools/task_tools.py +113 -29
  87. package/src/qingflow_mcp/tools/view_tools.py +106 -3
  88. package/src/qingflow_mcp/tools/workflow_tools.py +17 -1
  89. package/src/qingflow_mcp/tools/workspace_tools.py +71 -3
@@ -8,8 +8,11 @@ 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
- formatter = _FORMATTERS.get(hint, _format_generic)
12
- text = formatter(result)
11
+ if _is_failed_result(result) and hint != "task_action_execute":
12
+ text = _format_failed_result(result)
13
+ else:
14
+ formatter = _FORMATTERS.get(hint, _format_generic)
15
+ text = formatter(result)
13
16
  stream.write(text)
14
17
  if not text.endswith("\n"):
15
18
  stream.write("\n")
@@ -21,6 +24,50 @@ def _format_cancelled_result(result: dict[str, Any]) -> str | None:
21
24
  return str(result.get("message") or "已取消") + "\n"
22
25
 
23
26
 
27
+ def _is_failed_result(result: dict[str, Any]) -> bool:
28
+ if _is_executed_nonfatal_result(result):
29
+ return False
30
+ if result.get("ok") is False:
31
+ return True
32
+ return str(result.get("status") or "").lower() in {"failed", "blocked"}
33
+
34
+
35
+ def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
36
+ status = str(result.get("status") or "").lower()
37
+ executed = bool(
38
+ result.get("write_executed")
39
+ or result.get("delete_executed")
40
+ or result.get("action_executed")
41
+ or result.get("export_executed")
42
+ )
43
+ return executed and status in {"partial_success", "verification_failed", "running", "queued", "unknown"}
44
+
45
+
46
+ def _format_failed_result(result: dict[str, Any]) -> str:
47
+ lines = [f"Status: {result.get('status') or 'failed'}"]
48
+ for key, label in (
49
+ ("error_code", "Error Code"),
50
+ ("message", "Message"),
51
+ ("backend_code", "Backend Code"),
52
+ ("request_id", "Request ID"),
53
+ ("http_status", "HTTP Status"),
54
+ ("category", "Category"),
55
+ ):
56
+ value = result.get(key)
57
+ if value not in (None, ""):
58
+ lines.append(f"{label}: {value}")
59
+
60
+ data = result.get("data") if isinstance(result.get("data"), dict) else {}
61
+ selection = data.get("selection") if isinstance(data.get("selection"), dict) else {}
62
+ if selection:
63
+ lines.append("Selection:")
64
+ lines.extend(f"- {line}" for line in _dict_scalar_lines(selection))
65
+
66
+ _append_warnings(lines, result.get("warnings"))
67
+ _append_verification(lines, result.get("verification"))
68
+ return "\n".join(lines) + "\n"
69
+
70
+
24
71
  def _format_generic(result: dict[str, Any]) -> str:
25
72
  lines: list[str] = []
26
73
  title = _first_present(result, "status", "message")
@@ -292,6 +339,147 @@ def _format_record_get(result: dict[str, Any]) -> str:
292
339
  return "\n".join(lines) + "\n"
293
340
 
294
341
 
342
+ def _format_record_logs(result: dict[str, Any]) -> str:
343
+ app = result.get("app") if isinstance(result.get("app"), dict) else {}
344
+ view = result.get("view") if isinstance(result.get("view"), dict) else {}
345
+ record = result.get("record") if isinstance(result.get("record"), dict) else {}
346
+ data_logs = result.get("data_logs") if isinstance(result.get("data_logs"), dict) else {}
347
+ workflow_logs = result.get("workflow_logs") if isinstance(result.get("workflow_logs"), dict) else {}
348
+ integrity = result.get("context_integrity") if isinstance(result.get("context_integrity"), dict) else {}
349
+ lines = [
350
+ f"Status: {result.get('status') or '-'}",
351
+ f"App: {app.get('app_name') or app.get('app_key') or '-'} ({app.get('app_key') or '-'})",
352
+ f"View: {view.get('name') or view.get('view_id') or '-'}",
353
+ f"Record: {record.get('title') or '-'} ({record.get('record_id') or '-'})",
354
+ f"Data logs: {data_logs.get('status') or '-'} / count={data_logs.get('items_count')} / pages={data_logs.get('pages_fetched')} / complete={data_logs.get('complete')}",
355
+ f"Workflow logs: {workflow_logs.get('status') or '-'} / count={workflow_logs.get('items_count')} / pages={workflow_logs.get('pages_fetched')} / complete={workflow_logs.get('complete')}",
356
+ f"Safe for full log conclusion: {integrity.get('safe_for_full_log_conclusion')}",
357
+ ]
358
+ if result.get("local_dir"):
359
+ lines.append(f"Local dir: {result.get('local_dir')}")
360
+ if data_logs.get("local_path"):
361
+ lines.append(f"Data logs file: {data_logs.get('local_path')}")
362
+ if workflow_logs.get("local_path"):
363
+ lines.append(f"Workflow logs file: {workflow_logs.get('local_path')}")
364
+ if result.get("summary_path"):
365
+ lines.append(f"Summary file: {result.get('summary_path')}")
366
+ _append_warnings(lines, result.get("warnings"))
367
+ unavailable = result.get("unavailable_context") if isinstance(result.get("unavailable_context"), list) else []
368
+ if unavailable:
369
+ lines.append(f"Unavailable contexts: {len(unavailable)}")
370
+ return "\n".join(lines) + "\n"
371
+
372
+
373
+ def _format_record_write(result: dict[str, Any]) -> str:
374
+ status = str(result.get("status") or "").strip().lower()
375
+ write_executed = bool(result.get("write_executed"))
376
+ verification_status = str(result.get("verification_status") or "").strip().lower()
377
+ if write_executed and verification_status == "failed":
378
+ title = "写入已提交(回读验证未完成)"
379
+ elif write_executed and status in {"success", "completed"}:
380
+ title = "写入成功"
381
+ elif status:
382
+ title = status
383
+ else:
384
+ title = "写入结果"
385
+
386
+ lines = [title]
387
+ if result.get("record_id") not in (None, ""):
388
+ lines.append(f"Record ID: {result.get('record_id')}")
389
+ if result.get("apply_id") not in (None, "") and result.get("apply_id") != result.get("record_id"):
390
+ lines.append(f"Apply ID: {result.get('apply_id')}")
391
+
392
+ update_route = result.get("update_route") if isinstance(result.get("update_route"), dict) else {}
393
+ route_name = update_route.get("route") or update_route.get("type") or update_route.get("label")
394
+ if route_name:
395
+ lines.append(f"Update Route: {route_name}")
396
+
397
+ if verification_status:
398
+ lines.append(f"Verification: {verification_status}")
399
+ if write_executed:
400
+ lines.append("Safe To Retry: false")
401
+
402
+ if write_executed and verification_status == "failed":
403
+ verification = result.get("data", {}).get("verification") if isinstance(result.get("data"), dict) else None
404
+ if isinstance(verification, dict):
405
+ warning_codes = [
406
+ str(item.get("code"))
407
+ for item in verification.get("warnings", [])
408
+ if isinstance(item, dict) and item.get("code")
409
+ ]
410
+ if warning_codes:
411
+ lines.append("Verification Note: " + ", ".join(warning_codes[:3]))
412
+ lines.append("说明:写请求已执行;当前结果只表示后置回读未能确认字段值,不等同于写入被拒绝。")
413
+
414
+ _append_warnings(lines, result.get("warnings"))
415
+ return "\n".join(lines) + "\n"
416
+
417
+
418
+ def _format_record_delete(result: dict[str, Any]) -> str:
419
+ status = str(result.get("status") or "").strip().lower()
420
+ deleted_ids = [str(item) for item in result.get("deleted_ids", []) if item not in (None, "")]
421
+ failed_ids = [str(item) for item in result.get("failed_ids", []) if item not in (None, "")]
422
+ write_executed = bool(result.get("write_executed"))
423
+
424
+ if deleted_ids and failed_ids:
425
+ title = "删除部分完成"
426
+ elif deleted_ids:
427
+ title = "删除完成"
428
+ elif status == "failed" or result.get("ok") is False:
429
+ title = "删除未执行"
430
+ else:
431
+ title = "删除结果"
432
+
433
+ lines = [title]
434
+ lines.append(f"Deleted: {len(deleted_ids)}")
435
+ if deleted_ids:
436
+ lines.append("Deleted IDs: " + ", ".join(deleted_ids[:20]))
437
+ lines.append(f"Failed: {len(failed_ids)}")
438
+ if failed_ids:
439
+ lines.append("Failed IDs: " + ", ".join(failed_ids[:20]))
440
+ if write_executed:
441
+ lines.append("Safe To Retry: false")
442
+ elif result.get("safe_to_retry") is not None:
443
+ lines.append(f"Safe To Retry: {str(bool(result.get('safe_to_retry'))).lower()}")
444
+ _append_warnings(lines, result.get("warnings"))
445
+ return "\n".join(lines) + "\n"
446
+
447
+
448
+ def _format_code_block_run(result: dict[str, Any]) -> str:
449
+ status = str(result.get("status") or "").strip().lower()
450
+ execution = result.get("execution") if isinstance(result.get("execution"), dict) else {}
451
+ writeback = result.get("writeback") if isinstance(result.get("writeback"), dict) else {}
452
+ executed = bool(execution.get("executed"))
453
+ writeback_applied = bool(writeback.get("applied"))
454
+ write_verified = writeback.get("write_verified")
455
+
456
+ if executed and writeback_applied and status == "verification_failed":
457
+ title = "代码块已执行,回写已提交(验证未完成)"
458
+ elif executed and writeback_applied:
459
+ title = "代码块已执行,回写成功"
460
+ elif executed:
461
+ title = "代码块已执行"
462
+ elif status:
463
+ title = status
464
+ else:
465
+ title = "代码块结果"
466
+
467
+ lines = [title]
468
+ if result.get("record_id") not in (None, ""):
469
+ lines.append(f"Record ID: {result.get('record_id')}")
470
+ code_block_field = result.get("code_block_field") if isinstance(result.get("code_block_field"), dict) else {}
471
+ if code_block_field.get("title"):
472
+ lines.append(f"Code Block Field: {code_block_field.get('title')}")
473
+ if execution:
474
+ lines.append(f"Result Count: {execution.get('result_count', 0)}")
475
+ if writeback:
476
+ lines.append(f"Writeback: attempted={writeback.get('attempted')} applied={writeback_applied} verified={write_verified}")
477
+ if executed and writeback_applied and status == "verification_failed":
478
+ lines.append("说明:代码块执行和回写请求已完成;当前结果只表示后置回读未能确认字段值,不等同于回写被拒绝。")
479
+ _append_warnings(lines, result.get("warnings"))
480
+ return "\n".join(lines) + "\n"
481
+
482
+
295
483
  def _format_task_list(result: dict[str, Any]) -> str:
296
484
  data = result.get("data") if isinstance(result.get("data"), dict) else {}
297
485
  items = data.get("items") if isinstance(data.get("items"), list) else []
@@ -494,6 +682,42 @@ def _format_import_verify(result: dict[str, Any]) -> str:
494
682
  return "\n".join(lines) + "\n"
495
683
 
496
684
 
685
+ def _format_import_template(result: dict[str, Any]) -> str:
686
+ verification = result.get("verification") if isinstance(result.get("verification"), dict) else {}
687
+ template_source = verification.get("template_source")
688
+ if not template_source:
689
+ template_source = "official" if result.get("template_url") else ("local_generated" if result.get("downloaded_to_path") else "unknown")
690
+ if result.get("downloaded_to_path"):
691
+ title = "导入模板已生成" if template_source == "local_generated" else "导入模板已下载"
692
+ elif result.get("template_url"):
693
+ title = "导入模板已获取"
694
+ else:
695
+ title = "导入模板结果"
696
+
697
+ lines = [
698
+ title,
699
+ f"App Key: {result.get('app_key') or '-'}",
700
+ f"Template Source: {template_source}",
701
+ ]
702
+ if result.get("downloaded_to_path"):
703
+ lines.append(f"Local Path: {result.get('downloaded_to_path')}")
704
+ if result.get("template_url"):
705
+ lines.append(f"Template URL: {result.get('template_url')}")
706
+ import_capability = result.get("import_capability") if isinstance(result.get("import_capability"), dict) else {}
707
+ if import_capability:
708
+ lines.append(
709
+ "Import Capability: "
710
+ f"{import_capability.get('auth_source') or 'unknown'} / "
711
+ f"can_import={import_capability.get('can_import')}"
712
+ )
713
+ expected_columns = result.get("expected_columns") if isinstance(result.get("expected_columns"), list) else []
714
+ if expected_columns:
715
+ lines.append(f"Columns: {len(expected_columns)}")
716
+ _append_warnings(lines, result.get("warnings"))
717
+ _append_verification(lines, result.get("verification"))
718
+ return "\n".join(lines) + "\n"
719
+
720
+
497
721
  def _format_import_status(result: dict[str, Any]) -> str:
498
722
  lines = [
499
723
  f"Status: {result.get('status') or '-'}",
@@ -690,21 +914,23 @@ def _task_action_failure_label(action: str) -> str:
690
914
 
691
915
 
692
916
  def _task_action_partial_success_message(result: dict[str, Any]) -> str:
693
- error_code = str(result.get("error_code") or "").strip().upper()
694
- if error_code == "WORKFLOW_CONTINUATION_UNVERIFIED":
695
- return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
696
- if error_code == "TASK_ALREADY_PROCESSED":
697
- return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
698
917
  warnings = result.get("warnings")
699
918
  if isinstance(warnings, list):
700
919
  for warning in warnings:
701
920
  if not isinstance(warning, dict):
702
921
  continue
703
922
  code = str(warning.get("code") or "").strip().upper()
923
+ if code == "TASK_ACTION_VERIFICATION_PERMISSION_UNAVAILABLE":
924
+ return "动作已提交;后置验证读取受当前权限限制,不能据此判断动作被拒绝。可使用 --json 查看详细信息。"
704
925
  if code == "TASK_ALREADY_PROCESSED_UNCONFIRMED_ACTOR":
705
926
  return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
706
927
  if code == "WORKFLOW_CONTINUATION_UNVERIFIED":
707
928
  return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
929
+ error_code = str(result.get("error_code") or "").strip().upper()
930
+ if error_code == "WORKFLOW_CONTINUATION_UNVERIFIED":
931
+ return "动作已提交,但暂未完成后续流程验证。可使用 --json 查看详细信息。"
932
+ if error_code == "TASK_ALREADY_PROCESSED":
933
+ return "当前待办已不可操作,系统判断流程可能已被其他人处理。可使用 --json 查看详细信息。"
708
934
  return "动作已提交,但结果验证不完整。可使用 --json 查看详细信息。"
709
935
 
710
936
 
@@ -788,16 +1014,20 @@ _FORMATTERS = {
788
1014
  "workspace_get": _format_workspace_get,
789
1015
  "workspace_select": _format_workspace_select,
790
1016
  "app_list": _format_app_items,
791
- "app_search": _format_app_items,
792
1017
  "app_get": _format_app_get,
793
1018
  "record_list": _format_record_list,
794
1019
  "record_access": _format_record_access,
795
1020
  "record_get": _format_record_get,
1021
+ "record_logs": _format_record_logs,
1022
+ "record_write": _format_record_write,
1023
+ "record_delete": _format_record_delete,
1024
+ "code_block_run": _format_code_block_run,
796
1025
  "task_list": _format_task_list,
797
1026
  "task_workbench": _format_task_workbench,
798
1027
  "task_get": _format_task_get,
799
1028
  "task_action_execute": _format_task_action,
800
1029
  "task_associated_report_detail_get": _format_task_associated_report_detail,
1030
+ "import_template": _format_import_template,
801
1031
  "import_verify": _format_import_verify,
802
1032
  "import_status": _format_import_status,
803
1033
  "export_start": _format_export_start,
@@ -5,7 +5,7 @@ import json
5
5
  import sys
6
6
  from typing import Any, Callable, TextIO
7
7
 
8
- from ..errors import QingflowApiError
8
+ from ..errors import QingflowApiError, backend_code_value_int, message_looks_like_invalid_token
9
9
  from ..public_surface import cli_public_tool_spec_from_namespace
10
10
  from ..response_trim import resolve_cli_tool_name, trim_error_response, trim_public_response
11
11
  from ..tools.ai_builder_tools import _attach_builder_apply_envelope
@@ -323,6 +323,8 @@ def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, std
323
323
  f"Category: {payload.get('category') or 'error'}",
324
324
  f"Message: {payload.get('message') or 'Unknown error'}",
325
325
  ]
326
+ if payload.get("error_code"):
327
+ lines.append(f"Error Code: {payload.get('error_code')}")
326
328
  if payload.get("backend_code") is not None:
327
329
  lines.append(f"Backend Code: {payload.get('backend_code')}")
328
330
  if payload.get("request_id"):
@@ -337,23 +339,65 @@ def _emit_error(payload: dict[str, Any], *, json_mode: bool, stdout: TextIO, std
337
339
 
338
340
 
339
341
  def _error_exit_code(payload: dict[str, Any]) -> int:
340
- category = str(payload.get("category") or "").lower()
341
- if category in {"auth", "workspace"}:
342
+ if _is_auth_or_workspace_payload(payload):
342
343
  return 3
343
344
  return 4
344
345
 
345
346
 
347
+ def _is_auth_or_workspace_payload(payload: dict[str, Any]) -> bool:
348
+ category = str(payload.get("category") or "").lower()
349
+ error_code = str(payload.get("error_code") or "").upper()
350
+ http_status = backend_code_value_int(payload.get("http_status"))
351
+ if category in {"auth", "workspace"} or error_code in {"AUTH_REQUIRED", "WORKSPACE_NOT_SELECTED"}:
352
+ return True
353
+ if http_status == 401 or message_looks_like_invalid_token(payload.get("message")):
354
+ return True
355
+ return False
356
+
357
+
346
358
  def _result_exit_code(result: dict[str, Any]) -> int:
347
359
  if not isinstance(result, dict):
348
360
  return 0
361
+ if _is_executed_nonfatal_result(result):
362
+ return 0
363
+ if _is_auth_or_workspace_payload(result):
364
+ return 3
349
365
  if result.get("ok") is False:
350
366
  return 4
351
367
  status = str(result.get("status") or "").lower()
368
+ if status == "partial_success" and result.get("ok") is not True and not _has_readback_unavailable_verification(result):
369
+ return 4
352
370
  if status in {"failed", "blocked"}:
353
371
  return 4
354
372
  return 0
355
373
 
356
374
 
375
+ def _is_executed_nonfatal_result(result: dict[str, Any]) -> bool:
376
+ status = str(result.get("status") or "").lower()
377
+ executed = bool(
378
+ result.get("write_executed")
379
+ or result.get("delete_executed")
380
+ or result.get("action_executed")
381
+ or result.get("export_executed")
382
+ )
383
+ return executed and status in {"partial_success", "verification_failed", "running", "queued", "unknown"}
384
+
385
+
386
+ def _has_readback_unavailable_verification(result: dict[str, Any]) -> bool:
387
+ verification = result.get("verification")
388
+ if not isinstance(verification, dict):
389
+ return False
390
+ return any(
391
+ bool(verification.get(key))
392
+ for key in (
393
+ "readback_unavailable",
394
+ "readback_pending",
395
+ "metadata_unverified",
396
+ "views_read_unavailable",
397
+ )
398
+ )
399
+
400
+
357
401
  def _emit_cli_effective_context_notice(args: argparse.Namespace, context: CliContext, *, stream: TextIO) -> None:
358
402
  spec = cli_public_tool_spec_from_namespace(args)
359
403
  if spec is None or not spec.cli_show_effective_context:
@@ -9,11 +9,24 @@ from .json_types import JSONObject, JSONScalar
9
9
  INVALID_TOKEN_MARKERS = (
10
10
  "invalid token",
11
11
  "token invalid",
12
+ "token expired",
13
+ "expired token",
12
14
  "token失效",
13
15
  "无效token",
14
16
  "登录失效",
17
+ "登录过期",
18
+ "会话过期",
19
+ "session expired",
20
+ "session invalid",
15
21
  "login token invalid",
16
22
  "access token invalid",
23
+ "not logged in",
24
+ "not login",
25
+ "please login",
26
+ "please log in",
27
+ "未登录",
28
+ "请登录",
29
+ "重新登录",
17
30
  )
18
31
 
19
32
 
@@ -36,8 +49,7 @@ class QingflowApiError(Exception):
36
49
  return self.as_json()
37
50
 
38
51
  def looks_like_invalid_token(self) -> bool:
39
- text = self.message.lower()
40
- return any(marker in text for marker in INVALID_TOKEN_MARKERS)
52
+ return message_looks_like_invalid_token(self.message)
41
53
 
42
54
  @classmethod
43
55
  def auth_required(cls, profile: str) -> "QingflowApiError":
@@ -64,3 +76,32 @@ class QingflowApiError(Exception):
64
76
 
65
77
  def raise_tool_error(error: QingflowApiError) -> None:
66
78
  raise RuntimeError(error.as_json())
79
+
80
+
81
+ def backend_code_value_int(code: JSONScalar) -> int | None:
82
+ if isinstance(code, bool) or code is None:
83
+ return None
84
+ if isinstance(code, int):
85
+ return code
86
+ if isinstance(code, str):
87
+ text = code.strip()
88
+ if text:
89
+ try:
90
+ return int(text)
91
+ except ValueError:
92
+ return None
93
+ return None
94
+
95
+
96
+ def backend_code_int(error: QingflowApiError) -> int | None:
97
+ return backend_code_value_int(error.backend_code)
98
+
99
+
100
+ def message_looks_like_invalid_token(message: object) -> bool:
101
+ text = str(message or "").lower()
102
+ return any(marker in text for marker in INVALID_TOKEN_MARKERS)
103
+
104
+
105
+ def is_auth_like_error(error: QingflowApiError) -> bool:
106
+ category = str(error.category or "").strip().lower()
107
+ return category == "auth" or error.http_status == 401 or error.looks_like_invalid_token()
@@ -38,7 +38,6 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
38
38
  PublicToolSpec(USER_DOMAIN, "workspace_get", ("workspace_get",), ("workspace", "get")),
39
39
  PublicToolSpec(USER_DOMAIN, "workspace_select", ("workspace_select",), ("workspace", "select")),
40
40
  PublicToolSpec(USER_DOMAIN, "app_list", ("app_list",), ("app", "list"), cli_show_effective_context=True),
41
- PublicToolSpec(USER_DOMAIN, "app_search", ("app_search",), ("app", "search"), cli_show_effective_context=True),
42
41
  PublicToolSpec(USER_DOMAIN, "app_get", ("app_get",), ("app", "get"), cli_show_effective_context=True),
43
42
  PublicToolSpec(USER_DOMAIN, "portal_list", ("portal_list",), ("portal", "list"), cli_show_effective_context=True),
44
43
  PublicToolSpec(USER_DOMAIN, "portal_get", ("portal_get",), ("portal", "get"), cli_show_effective_context=True),
@@ -52,46 +51,52 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
52
51
  "record_schema_get",
53
52
  cli_route=("record", "schema", "applicant"),
54
53
  mcp_public=False,
54
+ cli_public=False,
55
55
  ),
56
56
  PublicToolSpec(
57
57
  USER_DOMAIN,
58
58
  "record_browse_schema_get",
59
59
  ("record_browse_schema_get_public",),
60
60
  ("record", "schema", "browse"),
61
+ cli_show_effective_context=True,
61
62
  ),
62
63
  PublicToolSpec(
63
64
  USER_DOMAIN,
64
65
  "record_insert_schema_get",
65
66
  ("record_insert_schema_get_public",),
66
67
  ("record", "schema", "insert"),
68
+ cli_show_effective_context=True,
67
69
  ),
68
70
  PublicToolSpec(
69
71
  USER_DOMAIN,
70
72
  "record_update_schema_get",
71
73
  ("record_update_schema_get_public",),
72
74
  ("record", "schema", "update"),
75
+ cli_show_effective_context=True,
73
76
  ),
74
- PublicToolSpec(USER_DOMAIN, "record_import_schema_get", ("record_import_schema_get",), ("record", "schema", "import")),
77
+ PublicToolSpec(USER_DOMAIN, "record_import_schema_get", ("record_import_schema_get",), ("record", "schema", "import"), cli_show_effective_context=True),
75
78
  PublicToolSpec(
76
79
  USER_DOMAIN,
77
80
  "record_code_block_schema_get",
78
81
  ("record_code_block_schema_get_public",),
79
82
  ("record", "schema", "code-block"),
83
+ cli_show_effective_context=True,
80
84
  ),
81
- PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), ("record", "member-candidates")),
82
- PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), ("record", "department-candidates")),
83
- PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False),
85
+ PublicToolSpec(USER_DOMAIN, "record_member_candidates", ("record_member_candidates",), ("record", "member-candidates"), cli_show_effective_context=True),
86
+ PublicToolSpec(USER_DOMAIN, "record_department_candidates", ("record_department_candidates",), ("record", "department-candidates"), cli_show_effective_context=True),
87
+ PublicToolSpec(USER_DOMAIN, "record_analyze", ("record_analyze",), ("record", "analyze"), mcp_public=False, cli_public=False),
84
88
  PublicToolSpec(USER_DOMAIN, "record_list", ("record_list",), ("record", "list"), cli_show_effective_context=True),
85
89
  PublicToolSpec(USER_DOMAIN, "record_access", ("record_access",), ("record", "access"), cli_show_effective_context=True),
86
90
  PublicToolSpec(USER_DOMAIN, "record_get", ("record_get_public",), ("record", "get"), cli_show_effective_context=True),
91
+ PublicToolSpec(USER_DOMAIN, "record_logs_get", ("record_logs_get",), ("record", "logs"), cli_show_effective_context=True),
87
92
  PublicToolSpec(USER_DOMAIN, "record_insert", ("record_insert_public",), ("record", "insert"), cli_show_effective_context=True, cli_context_write=True),
88
93
  PublicToolSpec(USER_DOMAIN, "record_update", ("record_update_public",), ("record", "update"), cli_show_effective_context=True, cli_context_write=True),
89
94
  PublicToolSpec(USER_DOMAIN, "record_delete", ("record_delete_public",), ("record", "delete"), cli_show_effective_context=True, cli_context_write=True),
90
- PublicToolSpec(USER_DOMAIN, "record_import_template_get", ("record_import_template_get",), ("import", "template")),
91
- PublicToolSpec(USER_DOMAIN, "record_import_verify", ("record_import_verify",), ("import", "verify")),
95
+ PublicToolSpec(USER_DOMAIN, "record_import_template_get", ("record_import_template_get",), ("import", "template"), cli_show_effective_context=True),
96
+ PublicToolSpec(USER_DOMAIN, "record_import_verify", ("record_import_verify",), ("import", "verify"), cli_show_effective_context=True),
92
97
  PublicToolSpec(USER_DOMAIN, "record_import_repair_local", ("record_import_repair_local",), ("import", "repair")),
93
- PublicToolSpec(USER_DOMAIN, "record_import_start", ("record_import_start",), ("import", "start")),
94
- PublicToolSpec(USER_DOMAIN, "record_import_status_get", ("record_import_status_get",), ("import", "status")),
98
+ PublicToolSpec(USER_DOMAIN, "record_import_start", ("record_import_start",), ("import", "start"), cli_show_effective_context=True, cli_context_write=True),
99
+ PublicToolSpec(USER_DOMAIN, "record_import_status_get", ("record_import_status_get",), ("import", "status"), cli_show_effective_context=True),
95
100
  PublicToolSpec(USER_DOMAIN, "record_export_start", ("record_export_start",), ("export", "start"), cli_show_effective_context=True),
96
101
  PublicToolSpec(USER_DOMAIN, "record_export_status_get", ("record_export_status_get",), ("export", "status"), cli_show_effective_context=True),
97
102
  PublicToolSpec(USER_DOMAIN, "record_export_get", ("record_export_get",), ("export", "get"), cli_show_effective_context=True),
@@ -109,12 +114,12 @@ USER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
109
114
  ),
110
115
  PublicToolSpec(USER_DOMAIN, "task_workflow_log_get", ("task_workflow_log_get",), ("task", "log"), cli_show_effective_context=True),
111
116
  PublicToolSpec(USER_DOMAIN, "directory_search", ("directory_search",), cli_public=False),
112
- PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), cli_public=False),
113
- PublicToolSpec(USER_DOMAIN, "directory_list_all_internal_users", ("directory_list_all_internal_users",), cli_public=False),
114
- PublicToolSpec(USER_DOMAIN, "directory_list_internal_departments", ("directory_list_internal_departments",), cli_public=False),
115
- PublicToolSpec(USER_DOMAIN, "directory_list_all_departments", ("directory_list_all_departments",), cli_public=False),
116
- PublicToolSpec(USER_DOMAIN, "directory_list_sub_departments", ("directory_list_sub_departments",), cli_public=False),
117
- PublicToolSpec(USER_DOMAIN, "directory_list_external_members", ("directory_list_external_members",), cli_public=False),
117
+ PublicToolSpec(USER_DOMAIN, "directory_list_internal_users", ("directory_list_internal_users",), mcp_public=False, cli_public=False),
118
+ PublicToolSpec(USER_DOMAIN, "directory_list_all_internal_users", ("directory_list_all_internal_users",), mcp_public=False, cli_public=False),
119
+ PublicToolSpec(USER_DOMAIN, "directory_list_internal_departments", ("directory_list_internal_departments",), mcp_public=False, cli_public=False),
120
+ PublicToolSpec(USER_DOMAIN, "directory_list_all_departments", ("directory_list_all_departments",), mcp_public=False, cli_public=False),
121
+ PublicToolSpec(USER_DOMAIN, "directory_list_sub_departments", ("directory_list_sub_departments",), mcp_public=False, cli_public=False),
122
+ PublicToolSpec(USER_DOMAIN, "directory_list_external_members", ("directory_list_external_members",), mcp_public=False, cli_public=False),
118
123
  )
119
124
 
120
125
 
@@ -127,6 +132,8 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
127
132
  PublicToolSpec(BUILDER_DOMAIN, "file_upload_local", ("file_upload_local",), ("builder", "file", "upload-local"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
128
133
  PublicToolSpec(BUILDER_DOMAIN, "feedback_submit", ("feedback_submit",), ("builder", "feedback", "submit"), has_contract=True),
129
134
  PublicToolSpec(BUILDER_DOMAIN, "builder_tool_contract", ("builder_tool_contract",), ("builder", "contract"), has_contract=False),
135
+ PublicToolSpec(BUILDER_DOMAIN, "workspace_icon_catalog_get", ("workspace_icon_catalog_get",), ("builder", "icon", "catalog"), has_contract=True, cli_show_effective_context=True),
136
+ PublicToolSpec(BUILDER_DOMAIN, "package_list", ("package_list",), ("builder", "package", "list"), has_contract=True, cli_show_effective_context=True),
130
137
  PublicToolSpec(BUILDER_DOMAIN, "package_get", ("package_get",), ("builder", "package", "get"), has_contract=True, cli_show_effective_context=True),
131
138
  PublicToolSpec(BUILDER_DOMAIN, "package_apply", ("package_apply",), ("builder", "package", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
132
139
  PublicToolSpec(BUILDER_DOMAIN, "solution_install", ("solution_install",), ("builder", "solution", "install"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
@@ -155,7 +162,7 @@ BUILDER_PUBLIC_TOOL_SPECS: tuple[PublicToolSpec, ...] = (
155
162
  PublicToolSpec(BUILDER_DOMAIN, "app_views_apply", ("app_views_apply",), ("builder", "views", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
156
163
  PublicToolSpec(BUILDER_DOMAIN, "app_charts_apply", ("app_charts_apply",), ("builder", "charts", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
157
164
  PublicToolSpec(BUILDER_DOMAIN, "portal_apply", ("portal_apply",), ("builder", "portal", "apply"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
158
- PublicToolSpec(BUILDER_DOMAIN, "app_publish_verify", ("app_publish_verify",), ("builder", "publish", "verify"), has_contract=True, cli_show_effective_context=True),
165
+ PublicToolSpec(BUILDER_DOMAIN, "app_publish_verify", ("app_publish_verify",), ("builder", "publish", "verify"), has_contract=True, cli_show_effective_context=True, cli_context_write=True),
159
166
  )
160
167
 
161
168
 
@@ -233,6 +240,7 @@ def cli_route_from_namespace(args: Namespace) -> tuple[str, ...] | None:
233
240
  builder_command = getattr(args, "builder_command", None)
234
241
  if not isinstance(builder_command, str) or not builder_command:
235
242
  return None
243
+ builder_command = "associated-resource" if builder_command == "associated-resources" else builder_command
236
244
  if builder_command == "contract":
237
245
  return ("builder", "contract")
238
246
  if builder_command == "app":