@josephyan/qingflow-app-builder-mcp 0.2.0-beta.998 → 0.2.0-beta.999
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-app-builder-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-builder-mcp@0.2.0-beta.999
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-builder-mcp@0.2.0-beta.999 qingflow-app-builder-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import json
|
|
4
5
|
|
|
5
6
|
from ..context import CliContext
|
|
7
|
+
from ..terminal_ui import SelectionOption, select_option
|
|
6
8
|
|
|
7
9
|
|
|
8
10
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
@@ -20,7 +22,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
20
22
|
get_parser.set_defaults(handler=_handle_get, format_hint="workspace_get")
|
|
21
23
|
|
|
22
24
|
select_parser = workspace_subparsers.add_parser("select", help="切换当前工作区")
|
|
23
|
-
select_parser.add_argument("--ws-id", type=int,
|
|
25
|
+
select_parser.add_argument("--ws-id", type=int, default=0, help="不传时在交互终端中选择工作区")
|
|
24
26
|
select_parser.set_defaults(handler=_handle_select, format_hint="workspace_get")
|
|
25
27
|
|
|
26
28
|
|
|
@@ -41,7 +43,68 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
|
|
46
|
+
if int(args.ws_id or 0) <= 0:
|
|
47
|
+
selected_ws_id = _choose_workspace_interactively(args, context)
|
|
48
|
+
if selected_ws_id is None:
|
|
49
|
+
raise RuntimeError(
|
|
50
|
+
json.dumps(
|
|
51
|
+
{
|
|
52
|
+
"category": "config",
|
|
53
|
+
"message": "workspace select requires --ws-id, or an interactive terminal to choose a workspace",
|
|
54
|
+
},
|
|
55
|
+
ensure_ascii=False,
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
args.ws_id = selected_ws_id
|
|
44
59
|
return context.workspace.workspace_select(
|
|
45
60
|
profile=args.profile,
|
|
46
61
|
ws_id=int(args.ws_id),
|
|
47
62
|
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _choose_workspace_interactively(args: argparse.Namespace, context: CliContext) -> int | None:
|
|
66
|
+
input_stream = getattr(args, "_stdin", None)
|
|
67
|
+
output_stream = getattr(args, "_stderr_stream", None)
|
|
68
|
+
if input_stream is None or output_stream is None:
|
|
69
|
+
return None
|
|
70
|
+
if not bool(getattr(input_stream, "isatty", lambda: False)()):
|
|
71
|
+
return None
|
|
72
|
+
page = context.workspace.workspace_list(
|
|
73
|
+
profile=args.profile,
|
|
74
|
+
page_num=1,
|
|
75
|
+
page_size=100,
|
|
76
|
+
include_external=False,
|
|
77
|
+
).get("page")
|
|
78
|
+
items = page.get("list") if isinstance(page, dict) and isinstance(page.get("list"), list) else []
|
|
79
|
+
if not items:
|
|
80
|
+
return None
|
|
81
|
+
current_ws_id = None
|
|
82
|
+
sessions = getattr(context, "sessions", None)
|
|
83
|
+
if sessions is not None and hasattr(sessions, "get_profile"):
|
|
84
|
+
try:
|
|
85
|
+
session_profile = sessions.get_profile(args.profile)
|
|
86
|
+
except Exception:
|
|
87
|
+
session_profile = None
|
|
88
|
+
current_ws_id = getattr(session_profile, "selected_ws_id", None) if session_profile is not None else None
|
|
89
|
+
options: list[SelectionOption[int]] = []
|
|
90
|
+
for item in items:
|
|
91
|
+
if not isinstance(item, dict):
|
|
92
|
+
continue
|
|
93
|
+
ws_id = int(item.get("wsId") or 0)
|
|
94
|
+
if ws_id <= 0:
|
|
95
|
+
continue
|
|
96
|
+
workspace_name = str(item.get("workspaceName") or item.get("wsName") or f"Workspace {ws_id}")
|
|
97
|
+
remark = str(item.get("remark") or "").strip()
|
|
98
|
+
label = workspace_name
|
|
99
|
+
if remark:
|
|
100
|
+
label = f"{workspace_name} - {remark}"
|
|
101
|
+
hint = f"ws_id={ws_id}"
|
|
102
|
+
if current_ws_id == ws_id:
|
|
103
|
+
hint += " · 当前"
|
|
104
|
+
options.append(SelectionOption(value=ws_id, label=label, hint=hint))
|
|
105
|
+
return select_option(
|
|
106
|
+
title="选择工作区",
|
|
107
|
+
options=options,
|
|
108
|
+
input_stream=input_stream,
|
|
109
|
+
output_stream=output_stream,
|
|
110
|
+
)
|
|
@@ -44,6 +44,9 @@ def run(
|
|
|
44
44
|
args = parser.parse_args(normalized_argv)
|
|
45
45
|
except SystemExit as exc:
|
|
46
46
|
return int(exc.code or 0)
|
|
47
|
+
setattr(args, "_stdin", sys.stdin)
|
|
48
|
+
setattr(args, "_stdout_stream", out)
|
|
49
|
+
setattr(args, "_stderr_stream", err)
|
|
47
50
|
handler = getattr(args, "handler", None)
|
|
48
51
|
if handler is None:
|
|
49
52
|
parser.print_help(out)
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import select
|
|
5
|
+
import shutil
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Generic, Sequence, TextIO, TypeVar
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import termios
|
|
11
|
+
import tty
|
|
12
|
+
except ImportError: # pragma: no cover - non-POSIX fallback
|
|
13
|
+
termios = None
|
|
14
|
+
tty = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class SelectionOption(Generic[T]):
|
|
22
|
+
value: T
|
|
23
|
+
label: str
|
|
24
|
+
hint: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def select_option(
|
|
28
|
+
*,
|
|
29
|
+
title: str,
|
|
30
|
+
options: Sequence[SelectionOption[T]],
|
|
31
|
+
input_stream: TextIO,
|
|
32
|
+
output_stream: TextIO,
|
|
33
|
+
page_size: int = 8,
|
|
34
|
+
) -> T | None:
|
|
35
|
+
if not options:
|
|
36
|
+
return None
|
|
37
|
+
if len(options) == 1:
|
|
38
|
+
return options[0].value
|
|
39
|
+
if not _supports_raw_selection(input_stream=input_stream, output_stream=output_stream):
|
|
40
|
+
return _select_option_via_prompt(title=title, options=options, input_stream=input_stream, output_stream=output_stream)
|
|
41
|
+
return _select_option_via_raw_terminal(
|
|
42
|
+
title=title,
|
|
43
|
+
options=options,
|
|
44
|
+
input_stream=input_stream,
|
|
45
|
+
output_stream=output_stream,
|
|
46
|
+
page_size=max(3, page_size),
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _supports_raw_selection(*, input_stream: TextIO, output_stream: TextIO) -> bool:
|
|
51
|
+
if termios is None or tty is None:
|
|
52
|
+
return False
|
|
53
|
+
if not bool(getattr(input_stream, "isatty", lambda: False)()) or not bool(getattr(output_stream, "isatty", lambda: False)()):
|
|
54
|
+
return False
|
|
55
|
+
return hasattr(input_stream, "fileno")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _select_option_via_prompt(
|
|
59
|
+
*,
|
|
60
|
+
title: str,
|
|
61
|
+
options: Sequence[SelectionOption[T]],
|
|
62
|
+
input_stream: TextIO,
|
|
63
|
+
output_stream: TextIO,
|
|
64
|
+
) -> T | None:
|
|
65
|
+
output_stream.write(title + "\n")
|
|
66
|
+
for index, option in enumerate(options, start=1):
|
|
67
|
+
suffix = f" ({option.hint})" if option.hint else ""
|
|
68
|
+
output_stream.write(f"{index}. {option.label}{suffix}\n")
|
|
69
|
+
output_stream.write("请输入编号并回车,留空取消: ")
|
|
70
|
+
output_stream.flush()
|
|
71
|
+
line = input_stream.readline()
|
|
72
|
+
selected = str(line or "").strip()
|
|
73
|
+
if not selected:
|
|
74
|
+
return None
|
|
75
|
+
if not selected.isdigit():
|
|
76
|
+
return None
|
|
77
|
+
index = int(selected) - 1
|
|
78
|
+
if index < 0 or index >= len(options):
|
|
79
|
+
return None
|
|
80
|
+
return options[index].value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _select_option_via_raw_terminal(
|
|
84
|
+
*,
|
|
85
|
+
title: str,
|
|
86
|
+
options: Sequence[SelectionOption[T]],
|
|
87
|
+
input_stream: TextIO,
|
|
88
|
+
output_stream: TextIO,
|
|
89
|
+
page_size: int,
|
|
90
|
+
) -> T | None:
|
|
91
|
+
fd = input_stream.fileno()
|
|
92
|
+
original_mode = termios.tcgetattr(fd)
|
|
93
|
+
selected_index = 0
|
|
94
|
+
output_stream.write("\x1b[?1049h\x1b[?25l")
|
|
95
|
+
output_stream.flush()
|
|
96
|
+
try:
|
|
97
|
+
tty.setraw(fd)
|
|
98
|
+
while True:
|
|
99
|
+
_render_options(
|
|
100
|
+
title=title,
|
|
101
|
+
options=options,
|
|
102
|
+
selected_index=selected_index,
|
|
103
|
+
output_stream=output_stream,
|
|
104
|
+
page_size=page_size,
|
|
105
|
+
)
|
|
106
|
+
key = _read_key(input_stream)
|
|
107
|
+
if key in ("\r", "\n"):
|
|
108
|
+
return options[selected_index].value
|
|
109
|
+
if key in ("\x03", "\x1b", "q", "Q"):
|
|
110
|
+
return None
|
|
111
|
+
if key in ("\x1b[A", "k", "K"):
|
|
112
|
+
selected_index = (selected_index - 1) % len(options)
|
|
113
|
+
continue
|
|
114
|
+
if key in ("\x1b[B", "j", "J"):
|
|
115
|
+
selected_index = (selected_index + 1) % len(options)
|
|
116
|
+
finally:
|
|
117
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, original_mode)
|
|
118
|
+
output_stream.write("\x1b[?25h\x1b[?1049l")
|
|
119
|
+
output_stream.flush()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _render_options(
|
|
123
|
+
*,
|
|
124
|
+
title: str,
|
|
125
|
+
options: Sequence[SelectionOption[object]],
|
|
126
|
+
selected_index: int,
|
|
127
|
+
output_stream: TextIO,
|
|
128
|
+
page_size: int,
|
|
129
|
+
) -> None:
|
|
130
|
+
terminal_width = shutil.get_terminal_size((100, 20)).columns
|
|
131
|
+
total = len(options)
|
|
132
|
+
page_start = max(0, min(selected_index - page_size // 2, max(total - page_size, 0)))
|
|
133
|
+
visible = options[page_start: page_start + page_size]
|
|
134
|
+
lines = [
|
|
135
|
+
title,
|
|
136
|
+
"↑/↓ 或 j/k 选择,Enter 确认,q / Esc 取消",
|
|
137
|
+
"",
|
|
138
|
+
]
|
|
139
|
+
for offset, option in enumerate(visible, start=page_start):
|
|
140
|
+
marker = ">" if offset == selected_index else " "
|
|
141
|
+
suffix = f" [{option.hint}]" if option.hint else ""
|
|
142
|
+
lines.append(_truncate_line(f"{marker} {option.label}{suffix}", width=terminal_width))
|
|
143
|
+
if total > page_size:
|
|
144
|
+
lines.append("")
|
|
145
|
+
lines.append(f"{selected_index + 1}/{total}")
|
|
146
|
+
output_stream.write("\x1b[2J\x1b[H")
|
|
147
|
+
output_stream.write("\n".join(lines))
|
|
148
|
+
output_stream.flush()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _truncate_line(text: str, *, width: int) -> str:
|
|
152
|
+
if width <= 0 or len(text) <= width:
|
|
153
|
+
return text
|
|
154
|
+
if width <= 1:
|
|
155
|
+
return text[:width]
|
|
156
|
+
return text[: width - 1] + "…"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _read_key(input_stream: TextIO) -> str:
|
|
160
|
+
first = input_stream.read(1)
|
|
161
|
+
if first != "\x1b":
|
|
162
|
+
return first
|
|
163
|
+
chunks = [first]
|
|
164
|
+
fd = input_stream.fileno()
|
|
165
|
+
while select.select([fd], [], [], 0.02)[0]:
|
|
166
|
+
chunk = os.read(fd, 1).decode("utf-8", errors="ignore")
|
|
167
|
+
if not chunk:
|
|
168
|
+
break
|
|
169
|
+
chunks.append(chunk)
|
|
170
|
+
if len(chunks) >= 3:
|
|
171
|
+
break
|
|
172
|
+
return "".join(chunks)
|