wxz-cli 1.0.0__tar.gz
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-1.0.0/LICENSE +21 -0
- wxz_cli-1.0.0/PKG-INFO +93 -0
- wxz_cli-1.0.0/README.md +63 -0
- wxz_cli-1.0.0/pyproject.toml +48 -0
- wxz_cli-1.0.0/setup.cfg +4 -0
- wxz_cli-1.0.0/src/wxz_cli/__init__.py +7 -0
- wxz_cli-1.0.0/src/wxz_cli/acp/__init__.py +1 -0
- wxz_cli-1.0.0/src/wxz_cli/acp/event_bridge.py +112 -0
- wxz_cli-1.0.0/src/wxz_cli/acp/handlers.py +113 -0
- wxz_cli-1.0.0/src/wxz_cli/acp/server.py +78 -0
- wxz_cli-1.0.0/src/wxz_cli/acp/session_manager.py +89 -0
- wxz_cli-1.0.0/src/wxz_cli/cli.py +87 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/__init__.py +1 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/acp_cmd.py +26 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/auth.py +95 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/chat.py +715 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/code.py +106 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/deploy.py +159 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/domain.py +359 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/preview.py +66 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/projects.py +105 -0
- wxz_cli-1.0.0/src/wxz_cli/commands/sandbox.py +70 -0
- wxz_cli-1.0.0/src/wxz_cli/core/__init__.py +1 -0
- wxz_cli-1.0.0/src/wxz_cli/core/chat_renderer.py +781 -0
- wxz_cli-1.0.0/src/wxz_cli/core/config.py +91 -0
- wxz_cli-1.0.0/src/wxz_cli/core/output.py +68 -0
- wxz_cli-1.0.0/src/wxz_cli/core/pop_client.py +613 -0
- wxz_cli-1.0.0/src/wxz_cli/core/session.py +154 -0
- wxz_cli-1.0.0/src/wxz_cli/core/ui.py +224 -0
- wxz_cli-1.0.0/src/wxz_cli/types.py +33 -0
- wxz_cli-1.0.0/src/wxz_cli.egg-info/PKG-INFO +93 -0
- wxz_cli-1.0.0/src/wxz_cli.egg-info/SOURCES.txt +38 -0
- wxz_cli-1.0.0/src/wxz_cli.egg-info/dependency_links.txt +1 -0
- wxz_cli-1.0.0/src/wxz_cli.egg-info/entry_points.txt +2 -0
- wxz_cli-1.0.0/src/wxz_cli.egg-info/requires.txt +6 -0
- wxz_cli-1.0.0/src/wxz_cli.egg-info/top_level.txt +1 -0
- wxz_cli-1.0.0/tests/test_chat.py +1069 -0
- wxz_cli-1.0.0/tests/test_chat_integration.py +112 -0
- wxz_cli-1.0.0/tests/test_domain.py +327 -0
- wxz_cli-1.0.0/tests/test_projects.py +152 -0
wxz_cli-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 MSEA AI Staff Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
wxz_cli-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wxz-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI for 万小智 (wxz) — AI-powered conversational website builder on Alibaba Cloud
|
|
5
|
+
Author: MSEA AI Staff Team
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aliyun/wxz-cli
|
|
8
|
+
Project-URL: Repository, https://github.com/aliyun/wxz-cli.git
|
|
9
|
+
Project-URL: Issues, https://github.com/aliyun/wxz-cli/issues
|
|
10
|
+
Keywords: wxz,website-builder,alibaba-cloud,cli,ai
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: click>=8.0
|
|
24
|
+
Requires-Dist: rich>=13.0
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
Requires-Dist: alibabacloud_tea_openapi>=0.3.0
|
|
27
|
+
Requires-Dist: alibabacloud_tea_util>=0.3.0
|
|
28
|
+
Requires-Dist: alibabacloud_credentials>=0.3.0
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# wxz-cli
|
|
32
|
+
|
|
33
|
+
CLI for 万小智 (wxz) — AI-powered conversational website builder on Alibaba Cloud.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install wxz-cli
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Login with Alibaba Cloud AK/SK
|
|
45
|
+
wxz login --ak <AccessKeyID> --sk <AccessKeySecret>
|
|
46
|
+
|
|
47
|
+
# Create a website via natural language
|
|
48
|
+
wxz chat start "帮我做一个科技公司官网"
|
|
49
|
+
|
|
50
|
+
# View project info
|
|
51
|
+
wxz projects info
|
|
52
|
+
|
|
53
|
+
# Deploy
|
|
54
|
+
wxz deploy deploy --watch
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Commands
|
|
58
|
+
|
|
59
|
+
| Command | Description |
|
|
60
|
+
|---------|-------------|
|
|
61
|
+
| `wxz login` | Authenticate with AK/SK |
|
|
62
|
+
| `wxz logout` | Clear local credentials |
|
|
63
|
+
| `wxz whoami` | Show current user info |
|
|
64
|
+
| `wxz chat start` | Create conversation and generate website |
|
|
65
|
+
| `wxz chat send` | Send follow-up message |
|
|
66
|
+
| `wxz chat generate` | Trigger code generation |
|
|
67
|
+
| `wxz chat status` | Show chat status |
|
|
68
|
+
| `wxz chat history` | Show chat history |
|
|
69
|
+
| `wxz projects list` | List all instances |
|
|
70
|
+
| `wxz projects use` | Bind project to directory |
|
|
71
|
+
| `wxz projects info` | Show project details |
|
|
72
|
+
| `wxz deploy deploy` | Publish website |
|
|
73
|
+
| `wxz deploy status` | Get publish status |
|
|
74
|
+
| `wxz domain bind` | Bind custom domain |
|
|
75
|
+
| `wxz domain dns` | Show DNS records |
|
|
76
|
+
| `wxz domain cert` | Set/delete SSL certificate |
|
|
77
|
+
| `wxz code tree` | Show sandbox directory tree |
|
|
78
|
+
| `wxz code cat` | Show file content |
|
|
79
|
+
| `wxz code rollback` | Rollback code snapshot |
|
|
80
|
+
|
|
81
|
+
## Configuration
|
|
82
|
+
|
|
83
|
+
### Environment Variables
|
|
84
|
+
|
|
85
|
+
| Variable | Description |
|
|
86
|
+
|----------|-------------|
|
|
87
|
+
| `WXZ_BIZ_ID` | Business/instance ID |
|
|
88
|
+
| `WXZ_CONVERSATION_ID` | Conversation ID |
|
|
89
|
+
| `WXZ_BASE_URL` | POP gateway base URL |
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
wxz_cli-1.0.0/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# wxz-cli
|
|
2
|
+
|
|
3
|
+
CLI for 万小智 (wxz) — AI-powered conversational website builder on Alibaba Cloud.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install wxz-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Login with Alibaba Cloud AK/SK
|
|
15
|
+
wxz login --ak <AccessKeyID> --sk <AccessKeySecret>
|
|
16
|
+
|
|
17
|
+
# Create a website via natural language
|
|
18
|
+
wxz chat start "帮我做一个科技公司官网"
|
|
19
|
+
|
|
20
|
+
# View project info
|
|
21
|
+
wxz projects info
|
|
22
|
+
|
|
23
|
+
# Deploy
|
|
24
|
+
wxz deploy deploy --watch
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
| Command | Description |
|
|
30
|
+
|---------|-------------|
|
|
31
|
+
| `wxz login` | Authenticate with AK/SK |
|
|
32
|
+
| `wxz logout` | Clear local credentials |
|
|
33
|
+
| `wxz whoami` | Show current user info |
|
|
34
|
+
| `wxz chat start` | Create conversation and generate website |
|
|
35
|
+
| `wxz chat send` | Send follow-up message |
|
|
36
|
+
| `wxz chat generate` | Trigger code generation |
|
|
37
|
+
| `wxz chat status` | Show chat status |
|
|
38
|
+
| `wxz chat history` | Show chat history |
|
|
39
|
+
| `wxz projects list` | List all instances |
|
|
40
|
+
| `wxz projects use` | Bind project to directory |
|
|
41
|
+
| `wxz projects info` | Show project details |
|
|
42
|
+
| `wxz deploy deploy` | Publish website |
|
|
43
|
+
| `wxz deploy status` | Get publish status |
|
|
44
|
+
| `wxz domain bind` | Bind custom domain |
|
|
45
|
+
| `wxz domain dns` | Show DNS records |
|
|
46
|
+
| `wxz domain cert` | Set/delete SSL certificate |
|
|
47
|
+
| `wxz code tree` | Show sandbox directory tree |
|
|
48
|
+
| `wxz code cat` | Show file content |
|
|
49
|
+
| `wxz code rollback` | Rollback code snapshot |
|
|
50
|
+
|
|
51
|
+
## Configuration
|
|
52
|
+
|
|
53
|
+
### Environment Variables
|
|
54
|
+
|
|
55
|
+
| Variable | Description |
|
|
56
|
+
|----------|-------------|
|
|
57
|
+
| `WXZ_BIZ_ID` | Business/instance ID |
|
|
58
|
+
| `WXZ_CONVERSATION_ID` | Conversation ID |
|
|
59
|
+
| `WXZ_BASE_URL` | POP gateway base URL |
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wxz-cli"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "CLI for 万小智 (wxz) — AI-powered conversational website builder on Alibaba Cloud"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "MSEA AI Staff Team"}
|
|
14
|
+
]
|
|
15
|
+
keywords = ["wxz", "website-builder", "alibaba-cloud", "cli", "ai"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Topic :: Software Development :: Build Tools",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"click>=8.0",
|
|
29
|
+
"rich>=13.0",
|
|
30
|
+
"httpx>=0.24.0",
|
|
31
|
+
"alibabacloud_tea_openapi>=0.3.0",
|
|
32
|
+
"alibabacloud_tea_util>=0.3.0",
|
|
33
|
+
"alibabacloud_credentials>=0.3.0",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
wxz = "wxz_cli.cli:cli"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/aliyun/wxz-cli"
|
|
41
|
+
Repository = "https://github.com/aliyun/wxz-cli.git"
|
|
42
|
+
Issues = "https://github.com/aliyun/wxz-cli/issues"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.package-dir]
|
|
48
|
+
"" = "src"
|
wxz_cli-1.0.0/setup.cfg
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
|
+
})
|
|
@@ -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)
|
|
@@ -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
|