@josephyan/qingflow-app-builder-mcp 0.2.0-beta.7 → 0.2.0-beta.71

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 (70) hide show
  1. package/README.md +5 -3
  2. package/docs/local-agent-install.md +21 -5
  3. package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
  4. package/npm/lib/runtime.mjs +168 -12
  5. package/package.json +1 -1
  6. package/pyproject.toml +4 -1
  7. package/skills/qingflow-app-builder/SKILL.md +155 -22
  8. package/skills/qingflow-app-builder/references/create-app.md +51 -21
  9. package/skills/qingflow-app-builder/references/environments.md +1 -1
  10. package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
  11. package/skills/qingflow-app-builder/references/gotchas.md +28 -1
  12. package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
  13. package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
  14. package/skills/qingflow-app-builder/references/update-flow.md +112 -25
  15. package/skills/qingflow-app-builder/references/update-layout.md +11 -24
  16. package/skills/qingflow-app-builder/references/update-schema.md +1 -23
  17. package/skills/qingflow-app-builder/references/update-views.md +87 -21
  18. package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
  19. package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
  20. package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
  21. package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
  22. package/src/qingflow_mcp/__init__.py +1 -1
  23. package/src/qingflow_mcp/backend_client.py +210 -0
  24. package/src/qingflow_mcp/builder_facade/models.py +1252 -3
  25. package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
  26. package/src/qingflow_mcp/cli/__init__.py +1 -0
  27. package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
  28. package/src/qingflow_mcp/cli/commands/app.py +40 -0
  29. package/src/qingflow_mcp/cli/commands/auth.py +78 -0
  30. package/src/qingflow_mcp/cli/commands/builder.py +515 -0
  31. package/src/qingflow_mcp/cli/commands/common.py +62 -0
  32. package/src/qingflow_mcp/cli/commands/imports.py +96 -0
  33. package/src/qingflow_mcp/cli/commands/record.py +304 -0
  34. package/src/qingflow_mcp/cli/commands/task.py +89 -0
  35. package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
  36. package/src/qingflow_mcp/cli/context.py +48 -0
  37. package/src/qingflow_mcp/cli/formatters.py +355 -0
  38. package/src/qingflow_mcp/cli/json_io.py +50 -0
  39. package/src/qingflow_mcp/cli/main.py +149 -0
  40. package/src/qingflow_mcp/config.py +39 -0
  41. package/src/qingflow_mcp/import_store.py +121 -0
  42. package/src/qingflow_mcp/list_type_labels.py +24 -0
  43. package/src/qingflow_mcp/response_trim.py +668 -0
  44. package/src/qingflow_mcp/server.py +160 -18
  45. package/src/qingflow_mcp/server_app_builder.py +275 -68
  46. package/src/qingflow_mcp/server_app_user.py +219 -191
  47. package/src/qingflow_mcp/session_store.py +41 -1
  48. package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
  49. package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
  50. package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
  51. package/src/qingflow_mcp/solution/executor.py +107 -11
  52. package/src/qingflow_mcp/solution/spec_models.py +2 -0
  53. package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
  54. package/src/qingflow_mcp/tools/app_tools.py +419 -12
  55. package/src/qingflow_mcp/tools/approval_tools.py +571 -72
  56. package/src/qingflow_mcp/tools/auth_tools.py +398 -2
  57. package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
  58. package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
  59. package/src/qingflow_mcp/tools/directory_tools.py +203 -31
  60. package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
  61. package/src/qingflow_mcp/tools/file_tools.py +1 -0
  62. package/src/qingflow_mcp/tools/import_tools.py +2150 -0
  63. package/src/qingflow_mcp/tools/package_tools.py +18 -4
  64. package/src/qingflow_mcp/tools/portal_tools.py +31 -0
  65. package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
  66. package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
  67. package/src/qingflow_mcp/tools/solution_tools.py +115 -3
  68. package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
  69. package/src/qingflow_mcp/tools/task_tools.py +376 -225
  70. package/src/qingflow_mcp/tools/workspace_tools.py +163 -19
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timedelta, timezone
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .config import get_mcp_home
11
+
12
+
13
+ def _utc_now() -> datetime:
14
+ return datetime.now(timezone.utc)
15
+
16
+
17
+ def _parse_utc(value: Any) -> datetime | None:
18
+ if not isinstance(value, str) or not value.strip():
19
+ return None
20
+ normalized = value.strip().replace("Z", "+00:00")
21
+ try:
22
+ parsed = datetime.fromisoformat(normalized)
23
+ except ValueError:
24
+ return None
25
+ if parsed.tzinfo is None:
26
+ return parsed.replace(tzinfo=timezone.utc)
27
+ return parsed.astimezone(timezone.utc)
28
+
29
+
30
+ def _json_safe_key(value: str) -> str:
31
+ keep = []
32
+ for char in value:
33
+ if char.isalnum() or char in {"-", "_"}:
34
+ keep.append(char)
35
+ else:
36
+ keep.append("_")
37
+ result = "".join(keep).strip("_")
38
+ return result or "entry"
39
+
40
+
41
+ def _store_dir(env_var: str, default_name: str) -> Path:
42
+ custom = os.getenv(env_var)
43
+ if custom:
44
+ return Path(custom).expanduser()
45
+ return get_mcp_home() / default_name
46
+
47
+
48
+ @dataclass(slots=True)
49
+ class _JsonEntryStore:
50
+ base_dir: Path
51
+ ttl: timedelta
52
+
53
+ def __post_init__(self) -> None:
54
+ self.base_dir.mkdir(parents=True, exist_ok=True)
55
+ self.prune()
56
+
57
+ def put(self, entry_id: str, payload: dict[str, Any]) -> None:
58
+ data = dict(payload)
59
+ data["id"] = entry_id
60
+ data["updated_at"] = _utc_now().isoformat()
61
+ self._path(entry_id).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
62
+
63
+ def get(self, entry_id: str) -> dict[str, Any] | None:
64
+ path = self._path(entry_id)
65
+ if not path.exists():
66
+ return None
67
+ try:
68
+ payload = json.loads(path.read_text(encoding="utf-8"))
69
+ except (OSError, json.JSONDecodeError):
70
+ path.unlink(missing_ok=True)
71
+ return None
72
+ created_at = _parse_utc(payload.get("created_at")) or _parse_utc(payload.get("updated_at"))
73
+ if created_at is None or _utc_now() - created_at > self.ttl:
74
+ path.unlink(missing_ok=True)
75
+ return None
76
+ return payload
77
+
78
+ def prune(self) -> None:
79
+ for path in self.base_dir.glob("*.json"):
80
+ try:
81
+ payload = json.loads(path.read_text(encoding="utf-8"))
82
+ except (OSError, json.JSONDecodeError):
83
+ path.unlink(missing_ok=True)
84
+ continue
85
+ created_at = _parse_utc(payload.get("created_at")) or _parse_utc(payload.get("updated_at"))
86
+ if created_at is None or _utc_now() - created_at > self.ttl:
87
+ path.unlink(missing_ok=True)
88
+
89
+ def list(self) -> list[dict[str, Any]]:
90
+ entries: list[dict[str, Any]] = []
91
+ self.prune()
92
+ for path in self.base_dir.glob("*.json"):
93
+ try:
94
+ payload = json.loads(path.read_text(encoding="utf-8"))
95
+ except (OSError, json.JSONDecodeError):
96
+ continue
97
+ created_at = _parse_utc(payload.get("created_at")) or _parse_utc(payload.get("updated_at"))
98
+ if created_at is None:
99
+ continue
100
+ entries.append(payload)
101
+ entries.sort(key=lambda item: item.get("created_at") or item.get("updated_at") or "", reverse=True)
102
+ return entries
103
+
104
+ def _path(self, entry_id: str) -> Path:
105
+ return self.base_dir / f"{_json_safe_key(entry_id)}.json"
106
+
107
+
108
+ class ImportVerificationStore(_JsonEntryStore):
109
+ def __init__(self, base_dir: Path | None = None, *, ttl_seconds: int = 3600) -> None:
110
+ super().__init__(
111
+ base_dir=base_dir or _store_dir("QINGFLOW_MCP_IMPORT_VERIFY_HOME", "import-verifications"),
112
+ ttl=timedelta(seconds=ttl_seconds),
113
+ )
114
+
115
+
116
+ class ImportJobStore(_JsonEntryStore):
117
+ def __init__(self, base_dir: Path | None = None, *, ttl_seconds: int = 24 * 3600) -> None:
118
+ super().__init__(
119
+ base_dir=base_dir or _store_dir("QINGFLOW_MCP_IMPORT_JOB_HOME", "import-jobs"),
120
+ ttl=timedelta(seconds=ttl_seconds),
121
+ )
@@ -20,6 +20,18 @@ RECORD_LIST_TYPE_LABELS: dict[int, str] = {
20
20
  16: "我发起的-已结束",
21
21
  }
22
22
 
23
+ SYSTEM_VIEW_DEFINITIONS: tuple[tuple[str, int, str], ...] = (
24
+ ("system:all", 8, "全部数据"),
25
+ ("system:initiated", 14, "我发起的"),
26
+ ("system:todo", 1, "待办"),
27
+ ("system:done", 2, "已办"),
28
+ ("system:cc", 12, "抄送我的"),
29
+ )
30
+
31
+ SYSTEM_VIEW_ID_TO_LIST_TYPE: dict[str, int] = {view_id: list_type for view_id, list_type, _ in SYSTEM_VIEW_DEFINITIONS}
32
+ SYSTEM_VIEW_ID_TO_NAME: dict[str, str] = {view_id: name for view_id, _, name in SYSTEM_VIEW_DEFINITIONS}
33
+ SYSTEM_LIST_TYPE_TO_VIEW_ID: dict[int, str] = {list_type: view_id for view_id, list_type, _ in SYSTEM_VIEW_DEFINITIONS}
34
+
23
35
  TASK_TYPE_LABELS: dict[int, str] = {
24
36
  1: "待办",
25
37
  2: "我发起的",
@@ -40,6 +52,18 @@ def get_record_list_type_label(list_type: int | None) -> str | None:
40
52
  return RECORD_LIST_TYPE_LABELS.get(list_type)
41
53
 
42
54
 
55
+ def get_system_view_id(list_type: int | None) -> str | None:
56
+ if list_type is None:
57
+ return None
58
+ return SYSTEM_LIST_TYPE_TO_VIEW_ID.get(list_type)
59
+
60
+
61
+ def get_system_view_name(view_id: str | None) -> str | None:
62
+ if view_id is None:
63
+ return None
64
+ return SYSTEM_VIEW_ID_TO_NAME.get(view_id)
65
+
66
+
43
67
  def get_task_type_label(type_value: int | None) -> str | None:
44
68
  if type_value is None:
45
69
  return None