mchat-client 0.1.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.
- mchat_client-0.1.0/PKG-INFO +106 -0
- mchat_client-0.1.0/README.md +89 -0
- mchat_client-0.1.0/pyproject.toml +28 -0
- mchat_client-0.1.0/setup.cfg +4 -0
- mchat_client-0.1.0/src/mchat_client/__init__.py +21 -0
- mchat_client-0.1.0/src/mchat_client/api.py +54 -0
- mchat_client-0.1.0/src/mchat_client/client.py +258 -0
- mchat_client-0.1.0/src/mchat_client.egg-info/PKG-INFO +106 -0
- mchat_client-0.1.0/src/mchat_client.egg-info/SOURCES.txt +10 -0
- mchat_client-0.1.0/src/mchat_client.egg-info/dependency_links.txt +1 -0
- mchat_client-0.1.0/src/mchat_client.egg-info/requires.txt +3 -0
- mchat_client-0.1.0/src/mchat_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mchat-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MChat Python 客户端 SDK
|
|
5
|
+
Classifier: Development Status :: 4 - Beta
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Communications :: Chat
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: paho-mqtt>=2.0.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
|
|
18
|
+
# MChat Python 客户端
|
|
19
|
+
|
|
20
|
+
Python 版 MChat 客户端 SDK,与《技术设计方案》及《消息交互接口与示例》一致。封装 MQTT 连接、请求-响应、收件箱/群消息订阅与事件。
|
|
21
|
+
|
|
22
|
+
## 要求
|
|
23
|
+
|
|
24
|
+
- Python >= 3.10
|
|
25
|
+
- 依赖:paho-mqtt >= 2.0.0
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd client/python
|
|
31
|
+
pip install -e .
|
|
32
|
+
# 或从项目外:pip install -e /path/to/MChat/client/python
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 连接参数
|
|
36
|
+
|
|
37
|
+
与 `employee.create` 返回的 `mqtt_connection` 对应,构造 `MChatClient` 时传入:
|
|
38
|
+
|
|
39
|
+
- `broker_host` / `broker_port` / `use_tls`
|
|
40
|
+
- `username`(如 employee_id)/ `password`
|
|
41
|
+
- `employee_id`:当前员工 ID,用于 auth.bind、收件箱订阅、在线状态
|
|
42
|
+
- 可选:`client_id`、`device_id`、`request_timeout_ms`、`skip_auth_bind`
|
|
43
|
+
|
|
44
|
+
## 使用示例
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from mchat_client import (
|
|
48
|
+
MChatClient,
|
|
49
|
+
send_private_message,
|
|
50
|
+
get_org_tree,
|
|
51
|
+
get_storage_config,
|
|
52
|
+
get_agent_capability_list,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
client = MChatClient(
|
|
56
|
+
broker_host="broker.example.com",
|
|
57
|
+
broker_port=1883,
|
|
58
|
+
use_tls=False,
|
|
59
|
+
username="emp_zhangsan_001",
|
|
60
|
+
password="your_mqtt_password",
|
|
61
|
+
employee_id="emp_zhangsan_001",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
client.connect()
|
|
65
|
+
|
|
66
|
+
client.on("inbox", lambda payload: print("收件箱:", payload))
|
|
67
|
+
client.on("group", lambda group_id, payload: print("群消息", group_id, payload))
|
|
68
|
+
|
|
69
|
+
# 发单聊
|
|
70
|
+
send_private_message(client, "emp_lisi_002", "你好")
|
|
71
|
+
|
|
72
|
+
# 获取组织树
|
|
73
|
+
tree = get_org_tree(client)
|
|
74
|
+
print(tree.get("data", {}).get("employees"))
|
|
75
|
+
|
|
76
|
+
# 订阅某群(需已知 group_id)
|
|
77
|
+
client.subscribe_group("grp_xxx")
|
|
78
|
+
|
|
79
|
+
client.disconnect()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API 概览
|
|
83
|
+
|
|
84
|
+
- **MChatClient**
|
|
85
|
+
- `connect()` / `disconnect()`
|
|
86
|
+
- `request(action, params)`:通用请求,成功返回完整响应体(含 code、message、data),失败抛异常
|
|
87
|
+
- `subscribe_group(group_id)` / `unsubscribe_group(group_id)`
|
|
88
|
+
- `on("inbox" | "group" | "connect" | "offline" | "error", callback)`
|
|
89
|
+
- **便捷方法**(见 `api.py`):`send_private_message`、`send_group_message`、`get_org_tree`、`get_storage_config`、`get_agent_capability_list`
|
|
90
|
+
|
|
91
|
+
## 示例
|
|
92
|
+
|
|
93
|
+
同目录下 **example/** 为可运行示例(连接、拉取组织架构与 Agent、收件箱/群消息、可选发测试消息)。详见 [example/README.md](example/README.md)。
|
|
94
|
+
|
|
95
|
+
## 发布到 PyPI
|
|
96
|
+
|
|
97
|
+
在 `client/python` 目录下构建并上传(需先安装 `build`、`twine`):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
cd client/python
|
|
101
|
+
pip install build twine
|
|
102
|
+
python -m build
|
|
103
|
+
twine upload dist/*
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
发布前请将 `pyproject.toml` 中的 `version` 更新为待发布版本号。
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# MChat Python 客户端
|
|
2
|
+
|
|
3
|
+
Python 版 MChat 客户端 SDK,与《技术设计方案》及《消息交互接口与示例》一致。封装 MQTT 连接、请求-响应、收件箱/群消息订阅与事件。
|
|
4
|
+
|
|
5
|
+
## 要求
|
|
6
|
+
|
|
7
|
+
- Python >= 3.10
|
|
8
|
+
- 依赖:paho-mqtt >= 2.0.0
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd client/python
|
|
14
|
+
pip install -e .
|
|
15
|
+
# 或从项目外:pip install -e /path/to/MChat/client/python
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## 连接参数
|
|
19
|
+
|
|
20
|
+
与 `employee.create` 返回的 `mqtt_connection` 对应,构造 `MChatClient` 时传入:
|
|
21
|
+
|
|
22
|
+
- `broker_host` / `broker_port` / `use_tls`
|
|
23
|
+
- `username`(如 employee_id)/ `password`
|
|
24
|
+
- `employee_id`:当前员工 ID,用于 auth.bind、收件箱订阅、在线状态
|
|
25
|
+
- 可选:`client_id`、`device_id`、`request_timeout_ms`、`skip_auth_bind`
|
|
26
|
+
|
|
27
|
+
## 使用示例
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from mchat_client import (
|
|
31
|
+
MChatClient,
|
|
32
|
+
send_private_message,
|
|
33
|
+
get_org_tree,
|
|
34
|
+
get_storage_config,
|
|
35
|
+
get_agent_capability_list,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
client = MChatClient(
|
|
39
|
+
broker_host="broker.example.com",
|
|
40
|
+
broker_port=1883,
|
|
41
|
+
use_tls=False,
|
|
42
|
+
username="emp_zhangsan_001",
|
|
43
|
+
password="your_mqtt_password",
|
|
44
|
+
employee_id="emp_zhangsan_001",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
client.connect()
|
|
48
|
+
|
|
49
|
+
client.on("inbox", lambda payload: print("收件箱:", payload))
|
|
50
|
+
client.on("group", lambda group_id, payload: print("群消息", group_id, payload))
|
|
51
|
+
|
|
52
|
+
# 发单聊
|
|
53
|
+
send_private_message(client, "emp_lisi_002", "你好")
|
|
54
|
+
|
|
55
|
+
# 获取组织树
|
|
56
|
+
tree = get_org_tree(client)
|
|
57
|
+
print(tree.get("data", {}).get("employees"))
|
|
58
|
+
|
|
59
|
+
# 订阅某群(需已知 group_id)
|
|
60
|
+
client.subscribe_group("grp_xxx")
|
|
61
|
+
|
|
62
|
+
client.disconnect()
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## API 概览
|
|
66
|
+
|
|
67
|
+
- **MChatClient**
|
|
68
|
+
- `connect()` / `disconnect()`
|
|
69
|
+
- `request(action, params)`:通用请求,成功返回完整响应体(含 code、message、data),失败抛异常
|
|
70
|
+
- `subscribe_group(group_id)` / `unsubscribe_group(group_id)`
|
|
71
|
+
- `on("inbox" | "group" | "connect" | "offline" | "error", callback)`
|
|
72
|
+
- **便捷方法**(见 `api.py`):`send_private_message`、`send_group_message`、`get_org_tree`、`get_storage_config`、`get_agent_capability_list`
|
|
73
|
+
|
|
74
|
+
## 示例
|
|
75
|
+
|
|
76
|
+
同目录下 **example/** 为可运行示例(连接、拉取组织架构与 Agent、收件箱/群消息、可选发测试消息)。详见 [example/README.md](example/README.md)。
|
|
77
|
+
|
|
78
|
+
## 发布到 PyPI
|
|
79
|
+
|
|
80
|
+
在 `client/python` 目录下构建并上传(需先安装 `build`、`twine`):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
cd client/python
|
|
84
|
+
pip install build twine
|
|
85
|
+
python -m build
|
|
86
|
+
twine upload dist/*
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
发布前请将 `pyproject.toml` 中的 `version` 更新为待发布版本号。
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mchat-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MChat Python 客户端 SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"paho-mqtt>=2.0.0",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Communications :: Chat",
|
|
23
|
+
]
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
dev = []
|
|
26
|
+
|
|
27
|
+
[tool.setuptools.packages.find]
|
|
28
|
+
where = ["src"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MChat Python 客户端 SDK
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from mchat_client.client import MChatClient
|
|
6
|
+
from mchat_client.api import (
|
|
7
|
+
send_private_message,
|
|
8
|
+
send_group_message,
|
|
9
|
+
get_org_tree,
|
|
10
|
+
get_storage_config,
|
|
11
|
+
get_agent_capability_list,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MChatClient",
|
|
16
|
+
"send_private_message",
|
|
17
|
+
"send_group_message",
|
|
18
|
+
"get_org_tree",
|
|
19
|
+
"get_storage_config",
|
|
20
|
+
"get_agent_capability_list",
|
|
21
|
+
]
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
便捷 API:基于 request 封装的常用方法
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional, Union
|
|
6
|
+
|
|
7
|
+
from mchat_client.client import MChatClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def send_private_message(
|
|
11
|
+
client: MChatClient,
|
|
12
|
+
to_employee_id: str,
|
|
13
|
+
content: Union[str, Dict[str, Any]],
|
|
14
|
+
quote_msg_id: Optional[str] = None,
|
|
15
|
+
) -> Dict[str, Any]:
|
|
16
|
+
"""发送私聊消息。"""
|
|
17
|
+
params: Dict[str, Any] = {"to_employee_id": to_employee_id, "content": content}
|
|
18
|
+
if quote_msg_id is not None:
|
|
19
|
+
params["quote_msg_id"] = quote_msg_id
|
|
20
|
+
return client.request("msg.send_private", params)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def send_group_message(
|
|
24
|
+
client: MChatClient,
|
|
25
|
+
group_id: str,
|
|
26
|
+
content: Union[str, Dict[str, Any]],
|
|
27
|
+
quote_msg_id: Optional[str] = None,
|
|
28
|
+
) -> Dict[str, Any]:
|
|
29
|
+
"""发送群消息。"""
|
|
30
|
+
params: Dict[str, Any] = {"group_id": group_id, "content": content}
|
|
31
|
+
if quote_msg_id is not None:
|
|
32
|
+
params["quote_msg_id"] = quote_msg_id
|
|
33
|
+
return client.request("msg.send_group", params)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_org_tree(client: MChatClient) -> Dict[str, Any]:
|
|
37
|
+
"""获取组织架构(部门、员工)。"""
|
|
38
|
+
return client.request("org.tree")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_storage_config(client: MChatClient) -> Dict[str, Any]:
|
|
42
|
+
"""获取存储配置。"""
|
|
43
|
+
return client.request("config.storage")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def get_agent_capability_list(
|
|
47
|
+
client: MChatClient,
|
|
48
|
+
skill: Optional[str] = None,
|
|
49
|
+
) -> Dict[str, Any]:
|
|
50
|
+
"""获取 Agent 能力列表。"""
|
|
51
|
+
params: Dict[str, Any] = {}
|
|
52
|
+
if skill is not None:
|
|
53
|
+
params["skill"] = skill
|
|
54
|
+
return client.request("agent.capability_list", params)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MChat Python 客户端:MQTT 连接、请求-响应、收件箱/群订阅与事件
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import random
|
|
7
|
+
import string
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
11
|
+
|
|
12
|
+
import paho.mqtt.client as mqtt
|
|
13
|
+
|
|
14
|
+
REQ_PREFIX = "mchat/msg/req/"
|
|
15
|
+
RESP_PREFIX = "mchat/msg/resp/"
|
|
16
|
+
INBOX_PREFIX = "mchat/inbox/"
|
|
17
|
+
GROUP_PREFIX = "mchat/group/"
|
|
18
|
+
STATUS_PREFIX = "mchat/status/"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _gen_seq_id() -> str:
|
|
22
|
+
return "seq_" + "".join(random.choices(string.ascii_lowercase + string.digits, k=10)) + "_" + str(int(time.time() * 1000))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _gen_client_id(employee_id: str, device_id: Optional[str] = None) -> str:
|
|
26
|
+
dev = device_id or "py"
|
|
27
|
+
uid = "".join(random.choices(string.ascii_lowercase + string.digits, k=8))
|
|
28
|
+
return f"{employee_id}_{dev}_{uid}"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class MChatClient:
|
|
32
|
+
"""MChat 客户端,与《消息交互接口与示例》一致。"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
broker_host: str,
|
|
37
|
+
broker_port: int,
|
|
38
|
+
username: str,
|
|
39
|
+
password: str,
|
|
40
|
+
employee_id: str,
|
|
41
|
+
use_tls: bool = False,
|
|
42
|
+
client_id: Optional[str] = None,
|
|
43
|
+
device_id: Optional[str] = None,
|
|
44
|
+
request_timeout_ms: int = 30000,
|
|
45
|
+
skip_auth_bind: bool = False,
|
|
46
|
+
):
|
|
47
|
+
self._broker_host = broker_host
|
|
48
|
+
self._broker_port = broker_port
|
|
49
|
+
self._use_tls = use_tls
|
|
50
|
+
self._username = username
|
|
51
|
+
self._password = password
|
|
52
|
+
self._employee_id = employee_id
|
|
53
|
+
self._client_id = (client_id or "").strip() or _gen_client_id(employee_id, device_id)
|
|
54
|
+
self._request_timeout_ms = request_timeout_ms
|
|
55
|
+
self._skip_auth_bind = skip_auth_bind
|
|
56
|
+
|
|
57
|
+
self._client: Optional[mqtt.Client] = None
|
|
58
|
+
self._pending: Dict[str, Dict[str, Any]] = {}
|
|
59
|
+
self._pending_lock = threading.Lock()
|
|
60
|
+
self._connect_event = threading.Event()
|
|
61
|
+
self._connect_error: Optional[Exception] = None
|
|
62
|
+
|
|
63
|
+
self._listeners: Dict[str, List[Callable[..., None]]] = {
|
|
64
|
+
"inbox": [],
|
|
65
|
+
"group": [],
|
|
66
|
+
"connect": [],
|
|
67
|
+
"offline": [],
|
|
68
|
+
"error": [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def connected(self) -> bool:
|
|
73
|
+
return self._client is not None and self._client.is_connected()
|
|
74
|
+
|
|
75
|
+
def get_client_id(self) -> str:
|
|
76
|
+
return self._client_id
|
|
77
|
+
|
|
78
|
+
def connect(self) -> None:
|
|
79
|
+
"""连接 Broker,订阅 resp/inbox,可选 auth.bind,发布 online。"""
|
|
80
|
+
self._connect_event.clear()
|
|
81
|
+
self._connect_error = None
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
client = mqtt.Client(
|
|
85
|
+
callback_api_version=getattr(mqtt.CallbackAPIVersion, "VERSION1", None) or mqtt.CallbackAPIVersion.VERSION1,
|
|
86
|
+
client_id=self._client_id,
|
|
87
|
+
protocol=mqtt.MQTTv311,
|
|
88
|
+
)
|
|
89
|
+
client.username_pw_set(self._username, self._password)
|
|
90
|
+
|
|
91
|
+
will_payload = json.dumps({"status": "offline", "updated_at": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())})
|
|
92
|
+
client.will_set(
|
|
93
|
+
f"{STATUS_PREFIX}{self._employee_id}",
|
|
94
|
+
will_payload,
|
|
95
|
+
qos=1,
|
|
96
|
+
retain=True,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
client.on_connect = self._on_connect
|
|
100
|
+
client.on_message = self._on_message
|
|
101
|
+
client.on_disconnect = self._on_disconnect
|
|
102
|
+
if hasattr(client, "on_connect_fail"):
|
|
103
|
+
client.on_connect_fail = self._on_connect_fail
|
|
104
|
+
|
|
105
|
+
if self._use_tls:
|
|
106
|
+
client.tls_set()
|
|
107
|
+
|
|
108
|
+
client.connect(self._broker_host, self._broker_port, 60)
|
|
109
|
+
self._client = client
|
|
110
|
+
client.loop_start()
|
|
111
|
+
|
|
112
|
+
if not self._connect_event.wait(timeout=15):
|
|
113
|
+
raise TimeoutError("Connection timeout")
|
|
114
|
+
if self._connect_error:
|
|
115
|
+
raise self._connect_error
|
|
116
|
+
|
|
117
|
+
except Exception:
|
|
118
|
+
self._client = None
|
|
119
|
+
raise
|
|
120
|
+
|
|
121
|
+
def _on_connect(self, client: mqtt.Client, userdata: Any, flags: Any, rc: int) -> None:
|
|
122
|
+
if rc != 0:
|
|
123
|
+
self._connect_error = ConnectionError(f"Connect failed: {rc}")
|
|
124
|
+
self._connect_event.set()
|
|
125
|
+
return
|
|
126
|
+
try:
|
|
127
|
+
client.subscribe(f"{RESP_PREFIX}{self._client_id}/+", qos=1)
|
|
128
|
+
client.subscribe(f"{INBOX_PREFIX}{self._employee_id}", qos=1)
|
|
129
|
+
online_payload = json.dumps({"status": "online", "updated_at": time.strftime("%Y-%m-%dT%H:%M:%S.000Z", time.gmtime())})
|
|
130
|
+
client.publish(f"{STATUS_PREFIX}{self._employee_id}", online_payload, qos=1, retain=True)
|
|
131
|
+
if not self._skip_auth_bind:
|
|
132
|
+
try:
|
|
133
|
+
r = self._request_impl(client, "auth.bind", {"employee_id": self._employee_id})
|
|
134
|
+
if r.get("code") != 0:
|
|
135
|
+
print(f"[mchat] auth.bind failed: {r.get('message')}")
|
|
136
|
+
except Exception as e:
|
|
137
|
+
print(f"[mchat] auth.bind error: {e}")
|
|
138
|
+
for fn in self._listeners["connect"]:
|
|
139
|
+
try:
|
|
140
|
+
fn()
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self._connect_error = e
|
|
145
|
+
finally:
|
|
146
|
+
self._connect_event.set()
|
|
147
|
+
|
|
148
|
+
def _on_message(self, client: mqtt.Client, userdata: Any, msg: Any) -> None:
|
|
149
|
+
topic = msg.topic
|
|
150
|
+
try:
|
|
151
|
+
payload_str = msg.payload.decode("utf-8") if isinstance(msg.payload, bytes) else msg.payload
|
|
152
|
+
except Exception:
|
|
153
|
+
return
|
|
154
|
+
if topic.startswith(RESP_PREFIX + self._client_id + "/"):
|
|
155
|
+
seq_id = topic[len(RESP_PREFIX + self._client_id + "/") :]
|
|
156
|
+
with self._pending_lock:
|
|
157
|
+
p = self._pending.pop(seq_id, None)
|
|
158
|
+
if p:
|
|
159
|
+
try:
|
|
160
|
+
body = json.loads(payload_str)
|
|
161
|
+
p["event"].set()
|
|
162
|
+
p["result"] = body
|
|
163
|
+
except Exception:
|
|
164
|
+
p["event"].set()
|
|
165
|
+
p["error"] = ValueError("Invalid response JSON")
|
|
166
|
+
return
|
|
167
|
+
if topic == f"{INBOX_PREFIX}{self._employee_id}":
|
|
168
|
+
try:
|
|
169
|
+
body = json.loads(payload_str)
|
|
170
|
+
for fn in self._listeners["inbox"]:
|
|
171
|
+
try:
|
|
172
|
+
fn(body)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
except Exception:
|
|
176
|
+
pass
|
|
177
|
+
return
|
|
178
|
+
if topic.startswith(GROUP_PREFIX):
|
|
179
|
+
group_id = topic[len(GROUP_PREFIX) :]
|
|
180
|
+
try:
|
|
181
|
+
body = json.loads(payload_str)
|
|
182
|
+
for fn in self._listeners["group"]:
|
|
183
|
+
try:
|
|
184
|
+
fn(group_id, body)
|
|
185
|
+
except Exception:
|
|
186
|
+
pass
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
def _on_disconnect(self, client: mqtt.Client, userdata: Any, rc: int) -> None:
|
|
191
|
+
for fn in self._listeners["offline"]:
|
|
192
|
+
try:
|
|
193
|
+
fn()
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def _on_connect_fail(self, client: mqtt.Client, userdata: Any) -> None:
|
|
198
|
+
self._connect_error = ConnectionError("Connect failed")
|
|
199
|
+
self._connect_event.set()
|
|
200
|
+
|
|
201
|
+
def _request_impl(self, client: mqtt.Client, action: str, params: Dict[str, Any]) -> Dict[str, Any]:
|
|
202
|
+
seq_id = _gen_seq_id()
|
|
203
|
+
topic = f"{REQ_PREFIX}{self._client_id}/{seq_id}"
|
|
204
|
+
payload = json.dumps({"action": action, **params})
|
|
205
|
+
event = threading.Event()
|
|
206
|
+
with self._pending_lock:
|
|
207
|
+
self._pending[seq_id] = {"event": event, "result": None, "error": None}
|
|
208
|
+
client.publish(topic, payload, qos=1)
|
|
209
|
+
timeout_s = self._request_timeout_ms / 1000.0
|
|
210
|
+
if not event.wait(timeout=timeout_s):
|
|
211
|
+
with self._pending_lock:
|
|
212
|
+
self._pending.pop(seq_id, None)
|
|
213
|
+
raise TimeoutError("Request timeout")
|
|
214
|
+
with self._pending_lock:
|
|
215
|
+
p = self._pending.get(seq_id)
|
|
216
|
+
if p and p.get("error"):
|
|
217
|
+
raise p["error"]
|
|
218
|
+
if p and p.get("result") is not None:
|
|
219
|
+
return p["result"]
|
|
220
|
+
raise RuntimeError("No response")
|
|
221
|
+
|
|
222
|
+
def request(self, action: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
223
|
+
"""发起一次请求,成功时返回 data 在其中的响应体;code!=0 或异常时抛错。"""
|
|
224
|
+
if not self._client or not self._client.is_connected():
|
|
225
|
+
raise RuntimeError("Not connected")
|
|
226
|
+
params = params or {}
|
|
227
|
+
r = self._request_impl(self._client, action, params)
|
|
228
|
+
if r.get("code") != 0:
|
|
229
|
+
raise RuntimeError(r.get("message") or f"code {r.get('code')}")
|
|
230
|
+
return r
|
|
231
|
+
|
|
232
|
+
def subscribe_group(self, group_id: str) -> None:
|
|
233
|
+
"""订阅群消息。"""
|
|
234
|
+
if not self._client or not self._client.is_connected():
|
|
235
|
+
raise RuntimeError("Not connected")
|
|
236
|
+
self._client.subscribe(f"{GROUP_PREFIX}{group_id}", qos=1)
|
|
237
|
+
|
|
238
|
+
def unsubscribe_group(self, group_id: str) -> None:
|
|
239
|
+
"""取消订阅群。"""
|
|
240
|
+
if self._client:
|
|
241
|
+
self._client.unsubscribe(f"{GROUP_PREFIX}{group_id}")
|
|
242
|
+
|
|
243
|
+
def on(self, event: str, fn: Callable[..., None]) -> None:
|
|
244
|
+
"""注册事件:inbox, group, connect, offline, error。"""
|
|
245
|
+
if event in self._listeners:
|
|
246
|
+
self._listeners[event].append(fn)
|
|
247
|
+
|
|
248
|
+
def disconnect(self) -> None:
|
|
249
|
+
"""断开连接。"""
|
|
250
|
+
if self._client:
|
|
251
|
+
with self._pending_lock:
|
|
252
|
+
for p in self._pending.values():
|
|
253
|
+
p["event"].set()
|
|
254
|
+
p["error"] = RuntimeError("Disconnected")
|
|
255
|
+
self._pending.clear()
|
|
256
|
+
self._client.loop_stop()
|
|
257
|
+
self._client.disconnect()
|
|
258
|
+
self._client = None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mchat-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MChat Python 客户端 SDK
|
|
5
|
+
Classifier: Development Status :: 4 - Beta
|
|
6
|
+
Classifier: Intended Audience :: Developers
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Topic :: Communications :: Chat
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: paho-mqtt>=2.0.0
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
|
|
18
|
+
# MChat Python 客户端
|
|
19
|
+
|
|
20
|
+
Python 版 MChat 客户端 SDK,与《技术设计方案》及《消息交互接口与示例》一致。封装 MQTT 连接、请求-响应、收件箱/群消息订阅与事件。
|
|
21
|
+
|
|
22
|
+
## 要求
|
|
23
|
+
|
|
24
|
+
- Python >= 3.10
|
|
25
|
+
- 依赖:paho-mqtt >= 2.0.0
|
|
26
|
+
|
|
27
|
+
## 安装
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd client/python
|
|
31
|
+
pip install -e .
|
|
32
|
+
# 或从项目外:pip install -e /path/to/MChat/client/python
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 连接参数
|
|
36
|
+
|
|
37
|
+
与 `employee.create` 返回的 `mqtt_connection` 对应,构造 `MChatClient` 时传入:
|
|
38
|
+
|
|
39
|
+
- `broker_host` / `broker_port` / `use_tls`
|
|
40
|
+
- `username`(如 employee_id)/ `password`
|
|
41
|
+
- `employee_id`:当前员工 ID,用于 auth.bind、收件箱订阅、在线状态
|
|
42
|
+
- 可选:`client_id`、`device_id`、`request_timeout_ms`、`skip_auth_bind`
|
|
43
|
+
|
|
44
|
+
## 使用示例
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from mchat_client import (
|
|
48
|
+
MChatClient,
|
|
49
|
+
send_private_message,
|
|
50
|
+
get_org_tree,
|
|
51
|
+
get_storage_config,
|
|
52
|
+
get_agent_capability_list,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
client = MChatClient(
|
|
56
|
+
broker_host="broker.example.com",
|
|
57
|
+
broker_port=1883,
|
|
58
|
+
use_tls=False,
|
|
59
|
+
username="emp_zhangsan_001",
|
|
60
|
+
password="your_mqtt_password",
|
|
61
|
+
employee_id="emp_zhangsan_001",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
client.connect()
|
|
65
|
+
|
|
66
|
+
client.on("inbox", lambda payload: print("收件箱:", payload))
|
|
67
|
+
client.on("group", lambda group_id, payload: print("群消息", group_id, payload))
|
|
68
|
+
|
|
69
|
+
# 发单聊
|
|
70
|
+
send_private_message(client, "emp_lisi_002", "你好")
|
|
71
|
+
|
|
72
|
+
# 获取组织树
|
|
73
|
+
tree = get_org_tree(client)
|
|
74
|
+
print(tree.get("data", {}).get("employees"))
|
|
75
|
+
|
|
76
|
+
# 订阅某群(需已知 group_id)
|
|
77
|
+
client.subscribe_group("grp_xxx")
|
|
78
|
+
|
|
79
|
+
client.disconnect()
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## API 概览
|
|
83
|
+
|
|
84
|
+
- **MChatClient**
|
|
85
|
+
- `connect()` / `disconnect()`
|
|
86
|
+
- `request(action, params)`:通用请求,成功返回完整响应体(含 code、message、data),失败抛异常
|
|
87
|
+
- `subscribe_group(group_id)` / `unsubscribe_group(group_id)`
|
|
88
|
+
- `on("inbox" | "group" | "connect" | "offline" | "error", callback)`
|
|
89
|
+
- **便捷方法**(见 `api.py`):`send_private_message`、`send_group_message`、`get_org_tree`、`get_storage_config`、`get_agent_capability_list`
|
|
90
|
+
|
|
91
|
+
## 示例
|
|
92
|
+
|
|
93
|
+
同目录下 **example/** 为可运行示例(连接、拉取组织架构与 Agent、收件箱/群消息、可选发测试消息)。详见 [example/README.md](example/README.md)。
|
|
94
|
+
|
|
95
|
+
## 发布到 PyPI
|
|
96
|
+
|
|
97
|
+
在 `client/python` 目录下构建并上传(需先安装 `build`、`twine`):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
cd client/python
|
|
101
|
+
pip install build twine
|
|
102
|
+
python -m build
|
|
103
|
+
twine upload dist/*
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
发布前请将 `pyproject.toml` 中的 `version` 更新为待发布版本号。
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/mchat_client/__init__.py
|
|
4
|
+
src/mchat_client/api.py
|
|
5
|
+
src/mchat_client/client.py
|
|
6
|
+
src/mchat_client.egg-info/PKG-INFO
|
|
7
|
+
src/mchat_client.egg-info/SOURCES.txt
|
|
8
|
+
src/mchat_client.egg-info/dependency_links.txt
|
|
9
|
+
src/mchat_client.egg-info/requires.txt
|
|
10
|
+
src/mchat_client.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mchat_client
|