@josephyan/qingflow-app-user-mcp 0.2.0-beta.1000 → 0.2.0-beta.1002
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-user-mcp@0.2.0-beta.
|
|
6
|
+
npm install @josephyan/qingflow-app-user-mcp@0.2.0-beta.1002
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Run:
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.
|
|
12
|
+
npx -y -p @josephyan/qingflow-app-user-mcp@0.2.0-beta.1002 qingflow-app-user-mcp
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Environment:
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -6,6 +6,8 @@ import json
|
|
|
6
6
|
from ..context import CliContext
|
|
7
7
|
from ..terminal_ui import SelectionOption, select_option
|
|
8
8
|
|
|
9
|
+
_INTERACTIVE_SELECTION_UNAVAILABLE = object()
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
11
13
|
parser = subparsers.add_parser("workspace", help="工作区")
|
|
@@ -23,7 +25,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
|
|
|
23
25
|
|
|
24
26
|
select_parser = workspace_subparsers.add_parser("select", help="切换当前工作区")
|
|
25
27
|
select_parser.add_argument("--ws-id", type=int, default=0, help="不传时在交互终端中选择工作区")
|
|
26
|
-
select_parser.set_defaults(handler=_handle_select, format_hint="
|
|
28
|
+
select_parser.set_defaults(handler=_handle_select, format_hint="workspace_select")
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
def _handle_list(args: argparse.Namespace, context: CliContext) -> dict:
|
|
@@ -45,7 +47,7 @@ def _handle_get(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
45
47
|
def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
|
|
46
48
|
if int(args.ws_id or 0) <= 0:
|
|
47
49
|
selected_ws_id = _choose_workspace_interactively(args, context)
|
|
48
|
-
if selected_ws_id is
|
|
50
|
+
if selected_ws_id is _INTERACTIVE_SELECTION_UNAVAILABLE:
|
|
49
51
|
raise RuntimeError(
|
|
50
52
|
json.dumps(
|
|
51
53
|
{
|
|
@@ -55,6 +57,8 @@ def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
55
57
|
ensure_ascii=False,
|
|
56
58
|
)
|
|
57
59
|
)
|
|
60
|
+
if selected_ws_id is None:
|
|
61
|
+
return {"status": "cancelled", "message": "已取消"}
|
|
58
62
|
args.ws_id = selected_ws_id
|
|
59
63
|
return context.workspace.workspace_select(
|
|
60
64
|
profile=args.profile,
|
|
@@ -62,13 +66,13 @@ def _handle_select(args: argparse.Namespace, context: CliContext) -> dict:
|
|
|
62
66
|
)
|
|
63
67
|
|
|
64
68
|
|
|
65
|
-
def _choose_workspace_interactively(args: argparse.Namespace, context: CliContext) -> int | None:
|
|
69
|
+
def _choose_workspace_interactively(args: argparse.Namespace, context: CliContext) -> int | None | object:
|
|
66
70
|
input_stream = getattr(args, "_stdin", None)
|
|
67
71
|
output_stream = getattr(args, "_stderr_stream", None)
|
|
68
72
|
if input_stream is None or output_stream is None:
|
|
69
|
-
return
|
|
73
|
+
return _INTERACTIVE_SELECTION_UNAVAILABLE
|
|
70
74
|
if not bool(getattr(input_stream, "isatty", lambda: False)()):
|
|
71
|
-
return
|
|
75
|
+
return _INTERACTIVE_SELECTION_UNAVAILABLE
|
|
72
76
|
page = context.workspace.workspace_list(
|
|
73
77
|
profile=args.profile,
|
|
74
78
|
page_num=1,
|
|
@@ -77,7 +81,7 @@ def _choose_workspace_interactively(args: argparse.Namespace, context: CliContex
|
|
|
77
81
|
).get("page")
|
|
78
82
|
items = page.get("list") if isinstance(page, dict) and isinstance(page.get("list"), list) else []
|
|
79
83
|
if not items:
|
|
80
|
-
return
|
|
84
|
+
return _INTERACTIVE_SELECTION_UNAVAILABLE
|
|
81
85
|
current_ws_id = None
|
|
82
86
|
sessions = getattr(context, "sessions", None)
|
|
83
87
|
if sessions is not None and hasattr(sessions, "get_profile"):
|
|
@@ -106,6 +106,21 @@ def _format_workspace_get(result: dict[str, Any]) -> str:
|
|
|
106
106
|
return "\n".join(lines) + "\n"
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def _format_workspace_select(result: dict[str, Any]) -> str:
|
|
110
|
+
status = str(result.get("status") or "").lower()
|
|
111
|
+
if status == "cancelled":
|
|
112
|
+
return str(result.get("message") or "已取消") + "\n"
|
|
113
|
+
|
|
114
|
+
workspace = result.get("workspace") if isinstance(result.get("workspace"), dict) else {}
|
|
115
|
+
workspace_name = workspace.get("workspaceName") or workspace.get("wsName") or result.get("selected", {}).get("workspace_name") if isinstance(result.get("selected"), dict) else None
|
|
116
|
+
lines = [
|
|
117
|
+
f"已切换到: {workspace_name or '-'} ({workspace.get('wsId') or result.get('ws_id') or '-'})",
|
|
118
|
+
f"QF Version: {result.get('qf_version') or workspace.get('systemVersion') or '-'}",
|
|
119
|
+
]
|
|
120
|
+
_append_warnings(lines, result.get("warnings"))
|
|
121
|
+
return "\n".join(lines) + "\n"
|
|
122
|
+
|
|
123
|
+
|
|
109
124
|
def _format_app_items(result: dict[str, Any]) -> str:
|
|
110
125
|
items = result.get("items")
|
|
111
126
|
if not isinstance(items, list):
|
|
@@ -559,6 +574,7 @@ _FORMATTERS = {
|
|
|
559
574
|
"auth_whoami": _format_whoami,
|
|
560
575
|
"workspace_list": _format_workspace_list,
|
|
561
576
|
"workspace_get": _format_workspace_get,
|
|
577
|
+
"workspace_select": _format_workspace_select,
|
|
562
578
|
"app_list": _format_app_items,
|
|
563
579
|
"app_search": _format_app_items,
|
|
564
580
|
"app_get": _format_app_get,
|
|
@@ -16,6 +16,10 @@ except ImportError: # pragma: no cover - non-POSIX fallback
|
|
|
16
16
|
|
|
17
17
|
T = TypeVar("T")
|
|
18
18
|
RAW_TTY_NEWLINE = "\r\n"
|
|
19
|
+
# Give terminal escape sequences a slightly roomier window so arrow keys
|
|
20
|
+
# still parse correctly when an outer Node/npm launcher adds a bit of PTY lag.
|
|
21
|
+
ESCAPE_SEQUENCE_TIMEOUT_SECONDS = 0.2
|
|
22
|
+
ESCAPE_SEQUENCE_FOLLOWUP_TIMEOUT_SECONDS = 0.02
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
@dataclass(slots=True)
|
|
@@ -158,16 +162,23 @@ def _truncate_line(text: str, *, width: int) -> str:
|
|
|
158
162
|
|
|
159
163
|
|
|
160
164
|
def _read_key(input_stream: TextIO) -> str:
|
|
161
|
-
|
|
165
|
+
fd = input_stream.fileno()
|
|
166
|
+
first_bytes = os.read(fd, 1)
|
|
167
|
+
if not first_bytes:
|
|
168
|
+
return ""
|
|
169
|
+
first = first_bytes.decode("utf-8", errors="ignore")
|
|
162
170
|
if first != "\x1b":
|
|
163
171
|
return first
|
|
164
172
|
chunks = [first]
|
|
165
|
-
|
|
166
|
-
|
|
173
|
+
if not select.select([fd], [], [], ESCAPE_SEQUENCE_TIMEOUT_SECONDS)[0]:
|
|
174
|
+
return first
|
|
175
|
+
while True:
|
|
167
176
|
chunk = os.read(fd, 1).decode("utf-8", errors="ignore")
|
|
168
177
|
if not chunk:
|
|
169
178
|
break
|
|
170
179
|
chunks.append(chunk)
|
|
171
180
|
if len(chunks) >= 3:
|
|
172
181
|
break
|
|
182
|
+
if not select.select([fd], [], [], ESCAPE_SEQUENCE_FOLLOWUP_TIMEOUT_SECONDS)[0]:
|
|
183
|
+
break
|
|
173
184
|
return "".join(chunks)
|