@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.
- package/README.md +5 -3
- package/docs/local-agent-install.md +21 -5
- package/npm/bin/qingflow-app-builder-mcp.mjs +1 -1
- package/npm/lib/runtime.mjs +168 -12
- package/package.json +1 -1
- package/pyproject.toml +4 -1
- package/skills/qingflow-app-builder/SKILL.md +155 -22
- package/skills/qingflow-app-builder/references/create-app.md +51 -21
- package/skills/qingflow-app-builder/references/environments.md +1 -1
- package/skills/qingflow-app-builder/references/flow-actors-and-permissions.md +123 -0
- package/skills/qingflow-app-builder/references/gotchas.md +28 -1
- package/skills/qingflow-app-builder/references/solution-playbooks.md +14 -12
- package/skills/qingflow-app-builder/references/tool-selection.md +47 -19
- package/skills/qingflow-app-builder/references/update-flow.md +112 -25
- package/skills/qingflow-app-builder/references/update-layout.md +11 -24
- package/skills/qingflow-app-builder/references/update-schema.md +1 -23
- package/skills/qingflow-app-builder/references/update-views.md +87 -21
- package/skills/qingflow-app-builder-code-integrations/SKILL.md +137 -0
- package/skills/qingflow-app-builder-code-integrations/agents/openai.yaml +4 -0
- package/skills/qingflow-app-builder-code-integrations/references/code-block.md +66 -0
- package/skills/qingflow-app-builder-code-integrations/references/q-linker.md +77 -0
- package/src/qingflow_mcp/__init__.py +1 -1
- package/src/qingflow_mcp/backend_client.py +210 -0
- package/src/qingflow_mcp/builder_facade/models.py +1252 -3
- package/src/qingflow_mcp/builder_facade/service.py +11367 -2389
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +515 -0
- package/src/qingflow_mcp/cli/commands/common.py +62 -0
- package/src/qingflow_mcp/cli/commands/imports.py +96 -0
- package/src/qingflow_mcp/cli/commands/record.py +304 -0
- package/src/qingflow_mcp/cli/commands/task.py +89 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +355 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +149 -0
- package/src/qingflow_mcp/config.py +39 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/list_type_labels.py +24 -0
- package/src/qingflow_mcp/response_trim.py +668 -0
- package/src/qingflow_mcp/server.py +160 -18
- package/src/qingflow_mcp/server_app_builder.py +275 -68
- package/src/qingflow_mcp/server_app_user.py +219 -191
- package/src/qingflow_mcp/session_store.py +41 -1
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +43 -4
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +119 -45
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +41 -2
- package/src/qingflow_mcp/solution/executor.py +107 -11
- package/src/qingflow_mcp/solution/spec_models.py +2 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2032 -127
- package/src/qingflow_mcp/tools/app_tools.py +419 -12
- package/src/qingflow_mcp/tools/approval_tools.py +571 -72
- package/src/qingflow_mcp/tools/auth_tools.py +398 -2
- package/src/qingflow_mcp/tools/code_block_tools.py +756 -0
- package/src/qingflow_mcp/tools/custom_button_tools.py +179 -0
- package/src/qingflow_mcp/tools/directory_tools.py +203 -31
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +1 -0
- package/src/qingflow_mcp/tools/import_tools.py +2150 -0
- package/src/qingflow_mcp/tools/package_tools.py +18 -4
- package/src/qingflow_mcp/tools/portal_tools.py +31 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +109 -7
- package/src/qingflow_mcp/tools/record_tools.py +9894 -1104
- package/src/qingflow_mcp/tools/solution_tools.py +115 -3
- package/src/qingflow_mcp/tools/task_context_tools.py +2040 -0
- package/src/qingflow_mcp/tools/task_tools.py +376 -225
- 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
|