wxz-cli 1.0.0__py3-none-any.whl
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.
- wxz_cli/__init__.py +7 -0
- wxz_cli/acp/__init__.py +1 -0
- wxz_cli/acp/event_bridge.py +112 -0
- wxz_cli/acp/handlers.py +113 -0
- wxz_cli/acp/server.py +78 -0
- wxz_cli/acp/session_manager.py +89 -0
- wxz_cli/cli.py +87 -0
- wxz_cli/commands/__init__.py +1 -0
- wxz_cli/commands/acp_cmd.py +26 -0
- wxz_cli/commands/auth.py +95 -0
- wxz_cli/commands/chat.py +715 -0
- wxz_cli/commands/code.py +106 -0
- wxz_cli/commands/deploy.py +159 -0
- wxz_cli/commands/domain.py +359 -0
- wxz_cli/commands/preview.py +66 -0
- wxz_cli/commands/projects.py +105 -0
- wxz_cli/commands/sandbox.py +70 -0
- wxz_cli/core/__init__.py +1 -0
- wxz_cli/core/chat_renderer.py +781 -0
- wxz_cli/core/config.py +91 -0
- wxz_cli/core/output.py +68 -0
- wxz_cli/core/pop_client.py +613 -0
- wxz_cli/core/session.py +154 -0
- wxz_cli/core/ui.py +224 -0
- wxz_cli/types.py +33 -0
- wxz_cli-1.0.0.dist-info/METADATA +93 -0
- wxz_cli-1.0.0.dist-info/RECORD +31 -0
- wxz_cli-1.0.0.dist-info/WHEEL +5 -0
- wxz_cli-1.0.0.dist-info/entry_points.txt +2 -0
- wxz_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
- wxz_cli-1.0.0.dist-info/top_level.txt +1 -0
wxz_cli/__init__.py
ADDED
wxz_cli/acp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""ACP (Agent Client Protocol) support for wxz-cli."""
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Chat event to ACP event bridge.
|
|
2
|
+
|
|
3
|
+
Consumes wxz SSE chat events (from CreateAppChat) and converts them
|
|
4
|
+
into ACP session/update notifications.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import threading
|
|
11
|
+
from typing import Generator
|
|
12
|
+
|
|
13
|
+
from wxz_cli.acp.server import ACPServer
|
|
14
|
+
from wxz_cli.core.pop_client import PopClient
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SSEtoACPBridge:
|
|
18
|
+
"""Consumes wxz SSE chat events and forwards them as ACP notifications."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, server: ACPServer, pop_client: PopClient, session_id: str):
|
|
21
|
+
self.server = server
|
|
22
|
+
self.pop_client = pop_client
|
|
23
|
+
self.session_id = session_id
|
|
24
|
+
self.last_event_id = 0
|
|
25
|
+
|
|
26
|
+
def start_stream(
|
|
27
|
+
self,
|
|
28
|
+
sse_stream: Generator[dict, None, None],
|
|
29
|
+
):
|
|
30
|
+
"""Start consuming SSE events in a background thread."""
|
|
31
|
+
self._thread = threading.Thread(
|
|
32
|
+
target=self._consume_stream,
|
|
33
|
+
args=(sse_stream,),
|
|
34
|
+
daemon=True,
|
|
35
|
+
)
|
|
36
|
+
self._thread.start()
|
|
37
|
+
|
|
38
|
+
def _consume_stream(self, sse_stream: Generator[dict, None, None]):
|
|
39
|
+
"""Consume SSE generator and emit ACP updates."""
|
|
40
|
+
for event in sse_stream:
|
|
41
|
+
self._emit_acp_update(event)
|
|
42
|
+
eid = event.get("id")
|
|
43
|
+
if eid is not None:
|
|
44
|
+
try:
|
|
45
|
+
self.last_event_id = int(eid)
|
|
46
|
+
except (ValueError, TypeError):
|
|
47
|
+
self.last_event_id = eid
|
|
48
|
+
|
|
49
|
+
def _emit_acp_update(self, event: dict):
|
|
50
|
+
"""Convert a single SSE event to an ACP session/update notification."""
|
|
51
|
+
name = event.get("name", "")
|
|
52
|
+
data = event.get("data", {})
|
|
53
|
+
event_id = event.get("id", 0)
|
|
54
|
+
|
|
55
|
+
match name:
|
|
56
|
+
case "message.delta":
|
|
57
|
+
if isinstance(data, dict):
|
|
58
|
+
content = data.get("content", "")
|
|
59
|
+
role = data.get("role", "")
|
|
60
|
+
ctype = data.get("contentType", "")
|
|
61
|
+
if content and role == "assistant" and ctype == "text":
|
|
62
|
+
self.server.send_notification("session/update", {
|
|
63
|
+
"sessionId": self.session_id,
|
|
64
|
+
"sessionUpdate": "agent_message_chunk",
|
|
65
|
+
"text": content,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
case "message.tool":
|
|
69
|
+
if isinstance(data, dict):
|
|
70
|
+
meta = data.get("metaData", {})
|
|
71
|
+
tool_name = meta.get("name", "") if isinstance(meta, dict) else ""
|
|
72
|
+
content = data.get("content", "")
|
|
73
|
+
self.server.send_notification("session/update", {
|
|
74
|
+
"sessionId": self.session_id,
|
|
75
|
+
"sessionUpdate": "tool_call",
|
|
76
|
+
"toolCallId": f"call_{event_id}",
|
|
77
|
+
"title": f"{tool_name}: {content}" if tool_name else content,
|
|
78
|
+
"kind": "edit",
|
|
79
|
+
"status": "completed",
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
case "message.interrupt":
|
|
83
|
+
self.server.send_notification("session/update", {
|
|
84
|
+
"sessionId": self.session_id,
|
|
85
|
+
"sessionUpdate": "agent_message_chunk",
|
|
86
|
+
"text": "[等待输入] AI 需要更多信息",
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
case "chat.completed":
|
|
90
|
+
self.server.send_notification("session/update", {
|
|
91
|
+
"sessionId": self.session_id,
|
|
92
|
+
"sessionUpdate": "plan",
|
|
93
|
+
"entries": [
|
|
94
|
+
{"content": "对话完成", "priority": "high", "status": "completed"},
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
case "message.error" | "error":
|
|
99
|
+
if isinstance(data, dict):
|
|
100
|
+
content = data.get("content", "")
|
|
101
|
+
try:
|
|
102
|
+
err = json.loads(content) if isinstance(content, str) else content
|
|
103
|
+
msg = err.get("errorMsg", content) if isinstance(err, dict) else content
|
|
104
|
+
except (json.JSONDecodeError, ValueError):
|
|
105
|
+
msg = data.get("errorMsg", str(data))
|
|
106
|
+
else:
|
|
107
|
+
msg = str(data)
|
|
108
|
+
self.server.send_notification("session/update", {
|
|
109
|
+
"sessionId": self.session_id,
|
|
110
|
+
"sessionUpdate": "agent_message_chunk",
|
|
111
|
+
"text": f"[错误] {msg}",
|
|
112
|
+
})
|
wxz_cli/acp/handlers.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""ACP method handlers for wxz-cli.
|
|
2
|
+
|
|
3
|
+
Registers JSON-RPC methods for the Agent Client Protocol.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
|
|
10
|
+
from wxz_cli.acp.server import ACPServer
|
|
11
|
+
from wxz_cli.acp.session_manager import ACPSessionManager
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ACPHandlers:
|
|
15
|
+
"""Registers ACP JSON-RPC method handlers."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, server: ACPServer, session_mgr: ACPSessionManager):
|
|
18
|
+
self.server = server
|
|
19
|
+
self.session_mgr = session_mgr
|
|
20
|
+
self._register_all()
|
|
21
|
+
|
|
22
|
+
def _register_all(self):
|
|
23
|
+
@self.server.method("initialize")
|
|
24
|
+
def handle_initialize(params):
|
|
25
|
+
return {
|
|
26
|
+
"protocolVersion": min(params.get("protocolVersion", 1), 1),
|
|
27
|
+
"agentCapabilities": {
|
|
28
|
+
"loadSession": True,
|
|
29
|
+
"promptCapabilities": {"image": True, "embeddedContext": True},
|
|
30
|
+
"auth": {"logout": {}},
|
|
31
|
+
"sessionCapabilities": {"list": True, "resume": True, "close": True},
|
|
32
|
+
},
|
|
33
|
+
"agentInfo": {
|
|
34
|
+
"name": "wxz",
|
|
35
|
+
"title": "万小智 AI 建站",
|
|
36
|
+
"version": "1.0.0",
|
|
37
|
+
},
|
|
38
|
+
"authMethods": [
|
|
39
|
+
{
|
|
40
|
+
"id": "aliyun-ak",
|
|
41
|
+
"name": "阿里云 AK/SK",
|
|
42
|
+
"description": "使用阿里云 AccessKey 认证",
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@self.server.method("authenticate")
|
|
48
|
+
def handle_authenticate(params):
|
|
49
|
+
ok = self.session_mgr.authenticate()
|
|
50
|
+
if not ok:
|
|
51
|
+
raise RuntimeError("Authentication required: set ALIBABACLOUD_ACCESS_KEY_ID and ALIBABACLOUD_ACCESS_KEY_SECRET, or run 'wxz login'")
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
@self.server.method("session/new")
|
|
55
|
+
def handle_session_new(params):
|
|
56
|
+
session_id = self.session_mgr.create_session(params.get("cwd", ""))
|
|
57
|
+
return {
|
|
58
|
+
"sessionId": session_id,
|
|
59
|
+
"configOptions": [
|
|
60
|
+
{
|
|
61
|
+
"id": "mode",
|
|
62
|
+
"name": "建站模式",
|
|
63
|
+
"description": "控制 AI 的行为方式",
|
|
64
|
+
"category": "mode",
|
|
65
|
+
"type": "select",
|
|
66
|
+
"currentValue": "build",
|
|
67
|
+
"options": [
|
|
68
|
+
{"value": "build", "name": "建站模式", "description": "AI 生成完整网站代码并部署"},
|
|
69
|
+
{"value": "ask", "name": "咨询模式", "description": "只回答问题,不修改代码"},
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@self.server.method("session/load")
|
|
76
|
+
def handle_session_load(params):
|
|
77
|
+
self.session_mgr.load_session(params["sessionId"], params.get("cwd", ""))
|
|
78
|
+
return {}
|
|
79
|
+
|
|
80
|
+
@self.server.method("session/list")
|
|
81
|
+
def handle_session_list(params):
|
|
82
|
+
# Return empty list for now; can be extended to query real instances
|
|
83
|
+
return {"sessions": []}
|
|
84
|
+
|
|
85
|
+
@self.server.method("session/prompt")
|
|
86
|
+
def handle_prompt(params):
|
|
87
|
+
session_id = params["sessionId"]
|
|
88
|
+
prompt_text = self._extract_text(params.get("prompt", []))
|
|
89
|
+
self.session_mgr.handle_prompt(session_id, prompt_text)
|
|
90
|
+
# In a full implementation, this would start polling
|
|
91
|
+
# ListAIStaffChatEvents and emit session/update notifications.
|
|
92
|
+
return {"stopReason": "end_turn"}
|
|
93
|
+
|
|
94
|
+
@self.server.method("session/cancel")
|
|
95
|
+
def handle_cancel(params):
|
|
96
|
+
self.session_mgr.cancel_session(params["sessionId"])
|
|
97
|
+
return {}
|
|
98
|
+
|
|
99
|
+
@self.server.method("logout")
|
|
100
|
+
def handle_logout(params):
|
|
101
|
+
self.session_mgr.logout()
|
|
102
|
+
return {}
|
|
103
|
+
|
|
104
|
+
def _extract_text(self, prompt_blocks: list) -> str:
|
|
105
|
+
"""Extract text from ACP content blocks."""
|
|
106
|
+
parts = []
|
|
107
|
+
for block in prompt_blocks:
|
|
108
|
+
if block.get("type") == "text":
|
|
109
|
+
parts.append(block.get("text", ""))
|
|
110
|
+
elif block.get("type") == "resource":
|
|
111
|
+
resource = block.get("resource", {})
|
|
112
|
+
parts.append(f"[参考文件: {resource.get('uri', '')}]")
|
|
113
|
+
return "\n".join(parts)
|
wxz_cli/acp/server.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""JSON-RPC 2.0 server over stdio for ACP mode."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Callable
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("wxz.acp")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ACPServer:
|
|
14
|
+
"""JSON-RPC 2.0 server based on stdio transport."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._methods: dict[str, Callable] = {}
|
|
18
|
+
self._initialized = False
|
|
19
|
+
|
|
20
|
+
def method(self, name: str):
|
|
21
|
+
"""Decorator to register a JSON-RPC method handler."""
|
|
22
|
+
def decorator(func):
|
|
23
|
+
self._methods[name] = func
|
|
24
|
+
return func
|
|
25
|
+
return decorator
|
|
26
|
+
|
|
27
|
+
def run(self):
|
|
28
|
+
"""Main loop: read JSON-RPC messages from stdin and dispatch."""
|
|
29
|
+
for line in sys.stdin:
|
|
30
|
+
line = line.strip()
|
|
31
|
+
if not line:
|
|
32
|
+
continue
|
|
33
|
+
try:
|
|
34
|
+
msg = json.loads(line)
|
|
35
|
+
except json.JSONDecodeError:
|
|
36
|
+
self._send_error(None, -32700, "Parse error")
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
method_name = msg.get("method")
|
|
40
|
+
msg_id = msg.get("id") # None for notifications
|
|
41
|
+
params = msg.get("params", {})
|
|
42
|
+
|
|
43
|
+
if method_name == "initialize":
|
|
44
|
+
self._initialized = True
|
|
45
|
+
|
|
46
|
+
if not self._initialized and method_name != "initialize":
|
|
47
|
+
if msg_id is not None:
|
|
48
|
+
self._send_error(msg_id, -32002, "Server not initialized")
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
if method_name in self._methods:
|
|
52
|
+
try:
|
|
53
|
+
result = self._methods[method_name](params)
|
|
54
|
+
if msg_id is not None:
|
|
55
|
+
self._send_result(msg_id, result)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.exception("Error handling %s", method_name)
|
|
58
|
+
if msg_id is not None:
|
|
59
|
+
self._send_error(msg_id, -32603, str(e))
|
|
60
|
+
else:
|
|
61
|
+
if msg_id is not None:
|
|
62
|
+
self._send_error(msg_id, -32601, f"Method not found: {method_name}")
|
|
63
|
+
|
|
64
|
+
def send_notification(self, method: str, params: dict):
|
|
65
|
+
"""Send a JSON-RPC notification (no id, no response needed)."""
|
|
66
|
+
msg = {"jsonrpc": "2.0", "method": method, "params": params}
|
|
67
|
+
sys.stdout.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
68
|
+
sys.stdout.flush()
|
|
69
|
+
|
|
70
|
+
def _send_result(self, msg_id, result):
|
|
71
|
+
msg = {"jsonrpc": "2.0", "id": msg_id, "result": result}
|
|
72
|
+
sys.stdout.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
73
|
+
sys.stdout.flush()
|
|
74
|
+
|
|
75
|
+
def _send_error(self, msg_id, code, message):
|
|
76
|
+
msg = {"jsonrpc": "2.0", "id": msg_id, "error": {"code": code, "message": message}}
|
|
77
|
+
sys.stdout.write(json.dumps(msg, ensure_ascii=False) + "\n")
|
|
78
|
+
sys.stdout.flush()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""ACP session manager for wxz-cli.
|
|
2
|
+
|
|
3
|
+
Manages ACP sessions mapped to wxz conversations.
|
|
4
|
+
Uses PopClient with alibabacloud SDK for API calls.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from wxz_cli.core.session import Session
|
|
13
|
+
from wxz_cli.core.pop_client import PopClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ACPSessionManager:
|
|
17
|
+
"""Manages ACP sessions and their mapping to wxz conversations."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, session: Session):
|
|
20
|
+
self.session = session
|
|
21
|
+
self._pop_client: Optional[PopClient] = None
|
|
22
|
+
self._sessions: dict[str, dict[str, Any]] = {}
|
|
23
|
+
|
|
24
|
+
def _get_client(self) -> PopClient:
|
|
25
|
+
if self._pop_client is None:
|
|
26
|
+
self._pop_client = PopClient(
|
|
27
|
+
access_key_id=self.session.access_key_id or None,
|
|
28
|
+
access_key_secret=self.session.access_key_secret or None,
|
|
29
|
+
security_token=self.session.security_token or None,
|
|
30
|
+
region=self.session.region_id,
|
|
31
|
+
)
|
|
32
|
+
return self._pop_client
|
|
33
|
+
|
|
34
|
+
def authenticate(self) -> bool:
|
|
35
|
+
"""Authenticate using session credentials or env vars."""
|
|
36
|
+
# Check if we have credentials from session or environment
|
|
37
|
+
ak = self.session.access_key_id
|
|
38
|
+
sk = self.session.access_key_secret
|
|
39
|
+
if not ak or not sk:
|
|
40
|
+
# The PopClient will use the default credential chain
|
|
41
|
+
# (env vars, shared config, ECS role) if no explicit credentials
|
|
42
|
+
pass
|
|
43
|
+
return True # PopClient handles credential resolution internally
|
|
44
|
+
|
|
45
|
+
def create_session(self, cwd: str) -> str:
|
|
46
|
+
"""Create a new wxz conversation and return session ID."""
|
|
47
|
+
client = self._get_client()
|
|
48
|
+
resp = client.create_ai_staff_conversation(text=f"New project in {cwd}")
|
|
49
|
+
module = resp.get("Module", resp)
|
|
50
|
+
session_id = module.get("ConversationId", "")
|
|
51
|
+
self._sessions[session_id] = {
|
|
52
|
+
"cwd": cwd,
|
|
53
|
+
"conversation_id": session_id,
|
|
54
|
+
"biz_id": module.get("SiteId"),
|
|
55
|
+
"chat_id": module.get("ChatId"),
|
|
56
|
+
"bot_id": module.get("BotId", "Zero2"),
|
|
57
|
+
}
|
|
58
|
+
return session_id
|
|
59
|
+
|
|
60
|
+
def load_session(self, session_id: str, cwd: str) -> None:
|
|
61
|
+
"""Load an existing session."""
|
|
62
|
+
self._sessions[session_id] = {
|
|
63
|
+
"cwd": cwd,
|
|
64
|
+
"conversation_id": session_id,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
def get_session(self, session_id: str) -> Optional[dict[str, Any]]:
|
|
68
|
+
return self._sessions.get(session_id)
|
|
69
|
+
|
|
70
|
+
def handle_prompt(self, session_id: str, prompt_text: str):
|
|
71
|
+
"""Handle a user prompt in an ACP session."""
|
|
72
|
+
sess = self.get_session(session_id)
|
|
73
|
+
if not sess:
|
|
74
|
+
raise RuntimeError(f"Session not found: {session_id}")
|
|
75
|
+
# Actual prompt handling is done by the caller (event bridge + poll)
|
|
76
|
+
return sess
|
|
77
|
+
|
|
78
|
+
def cancel_session(self, session_id: str) -> None:
|
|
79
|
+
self._sessions.pop(session_id, None)
|
|
80
|
+
|
|
81
|
+
def logout(self) -> None:
|
|
82
|
+
self.session.access_key_id = ""
|
|
83
|
+
self.session.access_key_secret = ""
|
|
84
|
+
self.session.save()
|
|
85
|
+
|
|
86
|
+
def close(self) -> None:
|
|
87
|
+
if self._pop_client:
|
|
88
|
+
self._pop_client.close()
|
|
89
|
+
self._pop_client = None
|
wxz_cli/cli.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""CLI entry point for wxz-cli.
|
|
3
|
+
|
|
4
|
+
A command-line interface for the wxz (Wan Xiao Zhi) AI-powered
|
|
5
|
+
conversational website builder on Alibaba Cloud.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
wxz --help
|
|
9
|
+
wxz login --ak <AK> --sk <SK>
|
|
10
|
+
wxz chat start "帮我做一个科技公司官网"
|
|
11
|
+
wxz deploy --watch
|
|
12
|
+
wxz acp --stdio
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
|
|
19
|
+
from wxz_cli import __version__
|
|
20
|
+
from wxz_cli.core.session import Session
|
|
21
|
+
|
|
22
|
+
from wxz_cli.commands.auth import login_cmd, logout_cmd, whoami_cmd
|
|
23
|
+
from wxz_cli.commands.chat import chat
|
|
24
|
+
from wxz_cli.commands.code import code
|
|
25
|
+
from wxz_cli.commands.projects import projects
|
|
26
|
+
from wxz_cli.commands.deploy import deploy
|
|
27
|
+
from wxz_cli.commands.domain import domain
|
|
28
|
+
from wxz_cli.commands.preview import preview_cmd
|
|
29
|
+
from wxz_cli.commands.sandbox import sandbox
|
|
30
|
+
from wxz_cli.commands.acp_cmd import acp_cmd
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@click.group()
|
|
34
|
+
@click.option("--base-url", envvar="WXZ_BASE_URL", default="https://websitebuild.aliyuncs.com",
|
|
35
|
+
help="POP gateway base URL (env: WXZ_BASE_URL).")
|
|
36
|
+
@click.option("--biz-id", envvar="WXZ_BIZ_ID", default=None,
|
|
37
|
+
help="Business/instance ID context (env: WXZ_BIZ_ID).")
|
|
38
|
+
@click.option("--conversation-id", envvar="WXZ_CONVERSATION_ID", default=None,
|
|
39
|
+
help="Conversation ID context (env: WXZ_CONVERSATION_ID).")
|
|
40
|
+
@click.option("--json", "json_mode", is_flag=True, default=False,
|
|
41
|
+
help="Output machine-readable JSON.")
|
|
42
|
+
@click.option("--dry-run", is_flag=True, default=False,
|
|
43
|
+
help="Show what would be done without executing.")
|
|
44
|
+
@click.version_option(version=__version__, prog_name="wxz")
|
|
45
|
+
@click.pass_context
|
|
46
|
+
def cli(ctx: click.Context, base_url: str, biz_id: str | None,
|
|
47
|
+
conversation_id: str | None, json_mode: bool, dry_run: bool) -> None:
|
|
48
|
+
"""wxz-cli — AI-powered conversational website builder.
|
|
49
|
+
|
|
50
|
+
Build, preview, and deploy websites via natural language conversation
|
|
51
|
+
with the 万小智 (Wan Xiao Zhi) AI agent on Alibaba Cloud.
|
|
52
|
+
"""
|
|
53
|
+
sess = Session.load()
|
|
54
|
+
|
|
55
|
+
# CLI flags override persisted session values
|
|
56
|
+
if base_url != "https://websitebuild.aliyuncs.com" or not sess.base_url:
|
|
57
|
+
sess.base_url = base_url
|
|
58
|
+
if biz_id:
|
|
59
|
+
sess.biz_id = biz_id
|
|
60
|
+
if conversation_id:
|
|
61
|
+
sess.conversation_id = conversation_id
|
|
62
|
+
|
|
63
|
+
ctx.ensure_object(dict)
|
|
64
|
+
ctx.obj["session"] = sess
|
|
65
|
+
ctx.obj["json_mode"] = json_mode
|
|
66
|
+
ctx.obj["dry_run"] = dry_run
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Register commands
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
cli.add_command(login_cmd)
|
|
74
|
+
cli.add_command(logout_cmd)
|
|
75
|
+
cli.add_command(whoami_cmd)
|
|
76
|
+
cli.add_command(chat)
|
|
77
|
+
cli.add_command(code)
|
|
78
|
+
cli.add_command(projects)
|
|
79
|
+
cli.add_command(deploy)
|
|
80
|
+
cli.add_command(domain)
|
|
81
|
+
cli.add_command(preview_cmd)
|
|
82
|
+
cli.add_command(sandbox)
|
|
83
|
+
cli.add_command(acp_cmd)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
cli()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command modules for wxz-cli."""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""ACP Agent mode command: acp --stdio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from wxz_cli.core.session import Session
|
|
8
|
+
from wxz_cli.acp.server import ACPServer
|
|
9
|
+
from wxz_cli.acp.handlers import ACPHandlers
|
|
10
|
+
from wxz_cli.acp.session_manager import ACPSessionManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command(name="acp")
|
|
14
|
+
@click.option("--stdio", is_flag=True, required=True, help="Run as ACP Agent over stdio.")
|
|
15
|
+
@click.pass_context
|
|
16
|
+
def acp_cmd(ctx, stdio):
|
|
17
|
+
"""Run wxz-cli as an ACP Agent (JSON-RPC over stdio)."""
|
|
18
|
+
if not stdio:
|
|
19
|
+
raise click.ClickException("ACP mode requires --stdio flag.")
|
|
20
|
+
|
|
21
|
+
sess = ctx.obj.get("session") or Session.load()
|
|
22
|
+
server = ACPServer()
|
|
23
|
+
session_mgr = ACPSessionManager(sess)
|
|
24
|
+
handlers = ACPHandlers(server, session_mgr)
|
|
25
|
+
|
|
26
|
+
server.run()
|
wxz_cli/commands/auth.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Authentication commands: login, logout, whoami."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from wxz_cli.core.session import Session
|
|
8
|
+
from wxz_cli.core.pop_client import PopClient
|
|
9
|
+
from wxz_cli.core.ui import print_success, print_error, print_info
|
|
10
|
+
from wxz_cli.core.output import format_api_response
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@click.command(name="login")
|
|
14
|
+
@click.option("--access-key-id", "--ak", help="Alibaba Cloud AccessKey ID.")
|
|
15
|
+
@click.option("--access-key-secret", "--sk", help="Alibaba Cloud AccessKey Secret.")
|
|
16
|
+
@click.option("--profile", default="default", help="Credential profile name.")
|
|
17
|
+
@click.option("--region", default="cn-hangzhou", help="Alibaba Cloud region.")
|
|
18
|
+
@click.option("--endpoint", default="websitebuild.aliyuncs.com", help="POP endpoint.")
|
|
19
|
+
@click.pass_context
|
|
20
|
+
def login_cmd(ctx, access_key_id, access_key_secret, profile, region, endpoint):
|
|
21
|
+
"""Authenticate with Alibaba Cloud AK/SK."""
|
|
22
|
+
json_mode = ctx.obj.get("json_mode", False)
|
|
23
|
+
|
|
24
|
+
ak = access_key_id or click.prompt("AccessKey ID", type=str)
|
|
25
|
+
sk = access_key_secret or click.prompt("AccessKey Secret", type=str, hide_input=True)
|
|
26
|
+
|
|
27
|
+
sess = Session.load()
|
|
28
|
+
sess.access_key_id = ak
|
|
29
|
+
sess.access_key_secret = sk
|
|
30
|
+
sess.region_id = region
|
|
31
|
+
sess.base_url = f"https://{endpoint}"
|
|
32
|
+
sess.save()
|
|
33
|
+
|
|
34
|
+
# Verify credentials by calling a lightweight read API
|
|
35
|
+
client = PopClient(
|
|
36
|
+
access_key_id=ak,
|
|
37
|
+
access_key_secret=sk,
|
|
38
|
+
endpoint=endpoint,
|
|
39
|
+
region=region,
|
|
40
|
+
)
|
|
41
|
+
try:
|
|
42
|
+
resp = client.list_app_instances(page_size=1)
|
|
43
|
+
if json_mode:
|
|
44
|
+
format_api_response({"success": True, "data": {"profile": profile}}, json_mode)
|
|
45
|
+
else:
|
|
46
|
+
print_success(f"Login successful! Profile: {profile}")
|
|
47
|
+
except Exception as e:
|
|
48
|
+
if json_mode:
|
|
49
|
+
format_api_response({"success": False, "error": str(e)}, json_mode)
|
|
50
|
+
else:
|
|
51
|
+
print_error(f"Login failed: {e}")
|
|
52
|
+
raise click.ClickException("Authentication failed.")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@click.command(name="logout")
|
|
56
|
+
@click.pass_context
|
|
57
|
+
def logout_cmd(ctx):
|
|
58
|
+
"""Clear local credentials."""
|
|
59
|
+
json_mode = ctx.obj.get("json_mode", False)
|
|
60
|
+
sess = Session.load()
|
|
61
|
+
sess.access_key_id = ""
|
|
62
|
+
sess.access_key_secret = ""
|
|
63
|
+
sess.security_token = ""
|
|
64
|
+
sess.biz_id = None
|
|
65
|
+
sess.conversation_id = None
|
|
66
|
+
sess.save()
|
|
67
|
+
if json_mode:
|
|
68
|
+
format_api_response({"success": True, "message": "Logged out"}, json_mode)
|
|
69
|
+
else:
|
|
70
|
+
print_success("Logged out. Local credentials cleared.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@click.command(name="whoami")
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def whoami_cmd(ctx):
|
|
76
|
+
"""Show current user info."""
|
|
77
|
+
json_mode = ctx.obj.get("json_mode", False)
|
|
78
|
+
sess = Session.load()
|
|
79
|
+
info = {
|
|
80
|
+
"access_key_id": sess.access_key_id[:8] + "..." if sess.access_key_id else None,
|
|
81
|
+
"region_id": sess.region_id,
|
|
82
|
+
"base_url": sess.base_url,
|
|
83
|
+
"biz_id": sess.biz_id,
|
|
84
|
+
"conversation_id": sess.conversation_id,
|
|
85
|
+
}
|
|
86
|
+
if json_mode:
|
|
87
|
+
format_api_response({"success": True, "data": info}, json_mode)
|
|
88
|
+
else:
|
|
89
|
+
print_info(f"AK: {info['access_key_id']}")
|
|
90
|
+
print_info(f"Region: {info['region_id']}")
|
|
91
|
+
print_info(f"Endpoint: {info['base_url']}")
|
|
92
|
+
if info['biz_id']:
|
|
93
|
+
print_info(f"Biz: {info['biz_id']}")
|
|
94
|
+
if info['conversation_id']:
|
|
95
|
+
print_info(f"Conversation: {info['conversation_id']}")
|