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 ADDED
@@ -0,0 +1,7 @@
1
+ """wxz-cli — CLI for 万小智 AI 建站平台.
2
+
3
+ A command-line interface for the wxz (Wan Xiao Zhi) conversational AI
4
+ website builder on Alibaba Cloud.
5
+ """
6
+
7
+ __version__ = "1.0.0"
@@ -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
+ })
@@ -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()
@@ -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']}")