aicq-hermes-plugin 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.
- aicq_hermes_plugin-1.0.0/LICENSE +21 -0
- aicq_hermes_plugin-1.0.0/PKG-INFO +122 -0
- aicq_hermes_plugin-1.0.0/README.md +94 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes/__init__.py +10 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes/adapter.py +307 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes/chat.py +246 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes/identity.py +117 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes/register.py +271 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes/server_client.py +307 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes_plugin.egg-info/PKG-INFO +122 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes_plugin.egg-info/SOURCES.txt +14 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes_plugin.egg-info/dependency_links.txt +1 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes_plugin.egg-info/requires.txt +4 -0
- aicq_hermes_plugin-1.0.0/aicq_hermes_plugin.egg-info/top_level.txt +1 -0
- aicq_hermes_plugin-1.0.0/pyproject.toml +40 -0
- aicq_hermes_plugin-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ctz168
|
|
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.
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: aicq-hermes-plugin
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: AICQ platform adapter plugin for Hermes agent — end-to-end encrypted chat with NaCl, friend management, file transfer, and tool calling
|
|
5
|
+
Author-email: ctz168 <ctz168@outlook.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://aicq.online
|
|
8
|
+
Project-URL: Repository, https://github.com/ctz168/pluginAICQ
|
|
9
|
+
Keywords: aicq,hermes,agent,encrypted-chat,e2ee,nacl,platform-adapter,plugin
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Communications :: Chat
|
|
19
|
+
Classifier: Topic :: Security :: Cryptography
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: aiohttp>=3.9
|
|
24
|
+
Requires-Dist: pynacl>=1.5
|
|
25
|
+
Requires-Dist: websockets>=12.0
|
|
26
|
+
Requires-Dist: aiosqlite>=0.19
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# AICQ Hermes Plugin
|
|
30
|
+
|
|
31
|
+
Connect [Hermes Agent](https://github.com/nousresearch/hermes-agent) to the [AICQ](https://aicq.online) end-to-end encrypted chat network.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- **Auto Registration & Login** — Ed25519 challenge-response authentication, registers on first run, reuses identity on subsequent starts
|
|
36
|
+
- **Master Binding** — Automatically adds the specified owner user as friend on startup
|
|
37
|
+
- **Text / File / Image Chat** — Full messaging support via WebSocket relay + REST fallback
|
|
38
|
+
- **Tool Calling** — 6 AICQ tools registered with Hermes (status, friends, chat send, history, file send)
|
|
39
|
+
- **Auto-Accept Friends** — Automatically accepts incoming friend requests
|
|
40
|
+
- **Unread Polling** — 30s periodic poll + WS reconnect fetch to never miss messages
|
|
41
|
+
- **E2EE** — NaCl (X25519 + XSalsa20-Poly1305) end-to-end encryption
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install aicq-hermes-plugin
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or install from source:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
cd pluginAICQ/hermes-plugin
|
|
53
|
+
pip install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration
|
|
57
|
+
|
|
58
|
+
Set environment variables or configure in `~/.hermes/.env`:
|
|
59
|
+
|
|
60
|
+
| Variable | Required | Default | Description |
|
|
61
|
+
|----------|----------|---------|-------------|
|
|
62
|
+
| `AICQ_SERVER_URL` | Yes | `https://aicq.online` | AICQ server URL |
|
|
63
|
+
| `AICQ_MASTER_NUMBER` | Yes | — | AICQ number of the master/owner to auto-bind |
|
|
64
|
+
| `AICQ_DATA_DIR` | No | `~/.aicq-hermes` | Directory for identity and data |
|
|
65
|
+
| `AICQ_AUTO_ACCEPT_FRIENDS` | No | `true` | Auto-accept friend requests |
|
|
66
|
+
|
|
67
|
+
## Hermes Plugin Setup
|
|
68
|
+
|
|
69
|
+
1. Install the plugin:
|
|
70
|
+
```bash
|
|
71
|
+
pip install aicq-hermes-plugin
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
2. Copy to Hermes plugins directory:
|
|
75
|
+
```bash
|
|
76
|
+
cp -r aicq_hermes/ ~/.hermes/plugins/aicq/
|
|
77
|
+
cp PLUGIN.yaml ~/.hermes/plugins/aicq/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
3. Configure environment:
|
|
81
|
+
```bash
|
|
82
|
+
# In ~/.hermes/.env
|
|
83
|
+
AICQ_SERVER_URL=https://aicq.online
|
|
84
|
+
AICQ_MASTER_NUMBER=1000000
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
4. Start Hermes with the AICQ platform:
|
|
88
|
+
```bash
|
|
89
|
+
hermes gateway run
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Registered Tools
|
|
93
|
+
|
|
94
|
+
| Tool | Description |
|
|
95
|
+
|------|-------------|
|
|
96
|
+
| `aicq_status` | Get connection status, agent ID, master info |
|
|
97
|
+
| `aicq_friends_list` | List all AICQ friends |
|
|
98
|
+
| `aicq_friends_add` | Add a friend by AICQ number |
|
|
99
|
+
| `aicq_chat_send` | Send a message (text/image/file) |
|
|
100
|
+
| `aicq_chat_history` | Get conversation history |
|
|
101
|
+
| `aicq_chat_send_file` | Send a file from local path |
|
|
102
|
+
|
|
103
|
+
## Architecture
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
Hermes Agent
|
|
107
|
+
│
|
|
108
|
+
├── AicqPlatformAdapter (BasePlatformAdapter)
|
|
109
|
+
│ ├── connect() → register/login + bind master + start WS
|
|
110
|
+
│ ├── disconnect() → close WS + stop polling
|
|
111
|
+
│ ├── send() → relay message to AICQ friend
|
|
112
|
+
│ └── set_message_handler() → forward inbound to Hermes
|
|
113
|
+
│
|
|
114
|
+
├── IdentityManager → Ed25519 + X25519 key persistence
|
|
115
|
+
├── AicqServerClient → REST API + WebSocket client
|
|
116
|
+
└── ChatManager → message dispatch, unread polling, file transfer
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## License
|
|
120
|
+
|
|
121
|
+
MIT
|
|
122
|
+
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# AICQ Hermes Plugin
|
|
2
|
+
|
|
3
|
+
Connect [Hermes Agent](https://github.com/nousresearch/hermes-agent) to the [AICQ](https://aicq.online) end-to-end encrypted chat network.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Auto Registration & Login** — Ed25519 challenge-response authentication, registers on first run, reuses identity on subsequent starts
|
|
8
|
+
- **Master Binding** — Automatically adds the specified owner user as friend on startup
|
|
9
|
+
- **Text / File / Image Chat** — Full messaging support via WebSocket relay + REST fallback
|
|
10
|
+
- **Tool Calling** — 6 AICQ tools registered with Hermes (status, friends, chat send, history, file send)
|
|
11
|
+
- **Auto-Accept Friends** — Automatically accepts incoming friend requests
|
|
12
|
+
- **Unread Polling** — 30s periodic poll + WS reconnect fetch to never miss messages
|
|
13
|
+
- **E2EE** — NaCl (X25519 + XSalsa20-Poly1305) end-to-end encryption
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install aicq-hermes-plugin
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install from source:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd pluginAICQ/hermes-plugin
|
|
25
|
+
pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Set environment variables or configure in `~/.hermes/.env`:
|
|
31
|
+
|
|
32
|
+
| Variable | Required | Default | Description |
|
|
33
|
+
|----------|----------|---------|-------------|
|
|
34
|
+
| `AICQ_SERVER_URL` | Yes | `https://aicq.online` | AICQ server URL |
|
|
35
|
+
| `AICQ_MASTER_NUMBER` | Yes | — | AICQ number of the master/owner to auto-bind |
|
|
36
|
+
| `AICQ_DATA_DIR` | No | `~/.aicq-hermes` | Directory for identity and data |
|
|
37
|
+
| `AICQ_AUTO_ACCEPT_FRIENDS` | No | `true` | Auto-accept friend requests |
|
|
38
|
+
|
|
39
|
+
## Hermes Plugin Setup
|
|
40
|
+
|
|
41
|
+
1. Install the plugin:
|
|
42
|
+
```bash
|
|
43
|
+
pip install aicq-hermes-plugin
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. Copy to Hermes plugins directory:
|
|
47
|
+
```bash
|
|
48
|
+
cp -r aicq_hermes/ ~/.hermes/plugins/aicq/
|
|
49
|
+
cp PLUGIN.yaml ~/.hermes/plugins/aicq/
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
3. Configure environment:
|
|
53
|
+
```bash
|
|
54
|
+
# In ~/.hermes/.env
|
|
55
|
+
AICQ_SERVER_URL=https://aicq.online
|
|
56
|
+
AICQ_MASTER_NUMBER=1000000
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
4. Start Hermes with the AICQ platform:
|
|
60
|
+
```bash
|
|
61
|
+
hermes gateway run
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Registered Tools
|
|
65
|
+
|
|
66
|
+
| Tool | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `aicq_status` | Get connection status, agent ID, master info |
|
|
69
|
+
| `aicq_friends_list` | List all AICQ friends |
|
|
70
|
+
| `aicq_friends_add` | Add a friend by AICQ number |
|
|
71
|
+
| `aicq_chat_send` | Send a message (text/image/file) |
|
|
72
|
+
| `aicq_chat_history` | Get conversation history |
|
|
73
|
+
| `aicq_chat_send_file` | Send a file from local path |
|
|
74
|
+
|
|
75
|
+
## Architecture
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
Hermes Agent
|
|
79
|
+
│
|
|
80
|
+
├── AicqPlatformAdapter (BasePlatformAdapter)
|
|
81
|
+
│ ├── connect() → register/login + bind master + start WS
|
|
82
|
+
│ ├── disconnect() → close WS + stop polling
|
|
83
|
+
│ ├── send() → relay message to AICQ friend
|
|
84
|
+
│ └── set_message_handler() → forward inbound to Hermes
|
|
85
|
+
│
|
|
86
|
+
├── IdentityManager → Ed25519 + X25519 key persistence
|
|
87
|
+
├── AicqServerClient → REST API + WebSocket client
|
|
88
|
+
└── ChatManager → message dispatch, unread polling, file transfer
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
94
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AICQ Hermes Plugin — Platform adapter for Hermes agent.
|
|
3
|
+
|
|
4
|
+
Connects Hermes to the AICQ end-to-end encrypted chat network.
|
|
5
|
+
Supports: login, registration, master binding, text/file/image chat, tool calling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .adapter import AicqPlatformAdapter
|
|
9
|
+
|
|
10
|
+
__all__ = ["AicqPlatformAdapter"]
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AICQ Platform Adapter for Hermes Agent.
|
|
3
|
+
|
|
4
|
+
Implements BasePlatformAdapter to connect Hermes to the AICQ
|
|
5
|
+
end-to-end encrypted chat network. Supports:
|
|
6
|
+
- Auto registration & login (Ed25519 challenge-response)
|
|
7
|
+
- Master binding (auto-add specified AICQ user as friend)
|
|
8
|
+
- Text, file, and image messaging
|
|
9
|
+
- Tool calling via AICQ gateway methods
|
|
10
|
+
- Friend request auto-accept
|
|
11
|
+
- Unread message polling on reconnect
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import logging
|
|
16
|
+
import os
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
from .identity import IdentityManager
|
|
20
|
+
from .server_client import AicqServerClient
|
|
21
|
+
from .chat import ChatManager
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger("aicq-hermes")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AicqPlatformAdapter:
|
|
27
|
+
"""
|
|
28
|
+
AICQ platform adapter for Hermes agent.
|
|
29
|
+
|
|
30
|
+
Usage in Hermes plugin register():
|
|
31
|
+
ctx.register_platform(
|
|
32
|
+
name="aicq",
|
|
33
|
+
label="AICQ Encrypted Chat",
|
|
34
|
+
adapter_factory=lambda cfg: AicqPlatformAdapter(cfg),
|
|
35
|
+
...
|
|
36
|
+
)
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: dict):
|
|
40
|
+
self.config = config
|
|
41
|
+
self.server_url = config.get("AICQ_SERVER_URL", "https://aicq.online")
|
|
42
|
+
self.master_number = config.get("AICQ_MASTER_NUMBER", "")
|
|
43
|
+
self.data_dir = config.get("AICQ_DATA_DIR", os.path.expanduser("~/.aicq-hermes"))
|
|
44
|
+
self.auto_accept = config.get("AICQ_AUTO_ACCEPT_FRIENDS", "true").lower() == "true"
|
|
45
|
+
self.agent_id = config.get("agent_id", "default")
|
|
46
|
+
|
|
47
|
+
os.makedirs(self.data_dir, exist_ok=True)
|
|
48
|
+
|
|
49
|
+
# Core components
|
|
50
|
+
self.identity = IdentityManager(os.path.join(self.data_dir, "identities"))
|
|
51
|
+
self.server = AicqServerClient(self.server_url, self.identity)
|
|
52
|
+
self.chat = ChatManager(self.server, self.data_dir)
|
|
53
|
+
|
|
54
|
+
# State
|
|
55
|
+
self._connected = False
|
|
56
|
+
self._master_bound = False
|
|
57
|
+
self._running = False
|
|
58
|
+
self._message_handler = None # Hermes handle_message callback
|
|
59
|
+
|
|
60
|
+
# ── Hermes Platform Adapter Interface ───────────────────────────────
|
|
61
|
+
|
|
62
|
+
async def connect(self) -> bool:
|
|
63
|
+
"""Connect to AICQ server: authenticate, bind master, start WS."""
|
|
64
|
+
try:
|
|
65
|
+
logger.info(f"Connecting to AICQ server: {self.server_url}")
|
|
66
|
+
|
|
67
|
+
# 1. Register or login
|
|
68
|
+
self.identity.get_or_create(self.agent_id, "Hermes AICQ Agent")
|
|
69
|
+
await self.server.ensure_auth(self.agent_id)
|
|
70
|
+
self.server._current_agent_id = self.agent_id
|
|
71
|
+
|
|
72
|
+
# 2. Bind master (add the owner as friend)
|
|
73
|
+
if self.master_number and not self._master_bound:
|
|
74
|
+
await self._bind_master()
|
|
75
|
+
|
|
76
|
+
# 3. Auto-accept pending friend requests
|
|
77
|
+
if self.auto_accept:
|
|
78
|
+
await self._auto_accept_friends()
|
|
79
|
+
|
|
80
|
+
# 4. Sync friends from server
|
|
81
|
+
await self._sync_friends()
|
|
82
|
+
|
|
83
|
+
# 5. Connect WebSocket
|
|
84
|
+
self.chat.set_on_new_message(self._on_inbound_message)
|
|
85
|
+
await self.server.connect_ws(self.agent_id)
|
|
86
|
+
|
|
87
|
+
# 6. Start unread polling
|
|
88
|
+
await self.chat.start_polling()
|
|
89
|
+
|
|
90
|
+
# 7. Fetch initial unread
|
|
91
|
+
await self._fetch_initial_unread()
|
|
92
|
+
|
|
93
|
+
self._connected = True
|
|
94
|
+
self._running = True
|
|
95
|
+
logger.info("AICQ connected successfully")
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.error(f"AICQ connection failed: {e}")
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
async def disconnect(self) -> None:
|
|
103
|
+
"""Disconnect from AICQ server."""
|
|
104
|
+
self._running = False
|
|
105
|
+
await self.chat.stop_polling()
|
|
106
|
+
await self.server.close()
|
|
107
|
+
self._connected = False
|
|
108
|
+
logger.info("AICQ disconnected")
|
|
109
|
+
|
|
110
|
+
async def send(self, chat_id: str, content: str, reply_to=None, metadata=None):
|
|
111
|
+
"""Send a message to a chat (friend or group).
|
|
112
|
+
|
|
113
|
+
This is the primary send method called by Hermes gateway.
|
|
114
|
+
"""
|
|
115
|
+
if not self._connected:
|
|
116
|
+
logger.warning("Not connected, cannot send message")
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
is_group = metadata.get("is_group", False) if metadata else False
|
|
120
|
+
msg_type = metadata.get("msg_type", "text") if metadata else "text"
|
|
121
|
+
|
|
122
|
+
# Handle file/image sending
|
|
123
|
+
file_path = metadata.get("file_path") if metadata else None
|
|
124
|
+
if file_path and os.path.exists(file_path):
|
|
125
|
+
success = await self.chat.send_file(chat_id, file_path)
|
|
126
|
+
if success:
|
|
127
|
+
return {"status": "sent", "type": "file"}
|
|
128
|
+
|
|
129
|
+
# Text message
|
|
130
|
+
success = await self.chat.send_message(
|
|
131
|
+
target_id=chat_id,
|
|
132
|
+
content=content,
|
|
133
|
+
msg_type=msg_type,
|
|
134
|
+
is_group=is_group,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if success:
|
|
138
|
+
return {"status": "sent", "type": msg_type}
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
async def send_typing(self, chat_id: str = None):
|
|
142
|
+
"""Send typing indicator (optional, not natively supported by AICQ)."""
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
def get_chat_info(self) -> dict:
|
|
146
|
+
"""Return platform metadata for Hermes."""
|
|
147
|
+
return {
|
|
148
|
+
"platform": "aicq",
|
|
149
|
+
"server_url": self.server_url,
|
|
150
|
+
"connected": self._connected,
|
|
151
|
+
"agent_id": self.agent_id,
|
|
152
|
+
"master_bound": self._master_bound,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# ── Master Binding ──────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
async def _bind_master(self):
|
|
158
|
+
"""Add the master/owner AICQ user as a friend automatically."""
|
|
159
|
+
try:
|
|
160
|
+
result = await self.server.add_friend_by_number(self.master_number)
|
|
161
|
+
status = result.get("status", "")
|
|
162
|
+
if status == "accepted" or result.get("to_id"):
|
|
163
|
+
self._master_bound = True
|
|
164
|
+
logger.info(f"Master bound: {self.master_number} (accepted)")
|
|
165
|
+
else:
|
|
166
|
+
logger.info(f"Master friend request sent: {self.master_number} (status: {status})")
|
|
167
|
+
self._master_bound = True # Request sent, will be accepted
|
|
168
|
+
except Exception as e:
|
|
169
|
+
logger.warning(f"Master bind failed: {e}")
|
|
170
|
+
|
|
171
|
+
# ── Friend Management ───────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
async def _auto_accept_friends(self):
|
|
174
|
+
"""Auto-accept pending friend requests."""
|
|
175
|
+
try:
|
|
176
|
+
requests = await self.server.list_friend_requests()
|
|
177
|
+
for req in requests:
|
|
178
|
+
req_id = req.get("id") or req.get("request_id") or req.get("session_id")
|
|
179
|
+
if req_id:
|
|
180
|
+
try:
|
|
181
|
+
await self.server.accept_friend_request(req_id)
|
|
182
|
+
logger.info(f"Auto-accepted friend request: {req_id}")
|
|
183
|
+
except Exception as e:
|
|
184
|
+
logger.warning(f"Auto-accept failed for {req_id}: {e}")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.warning(f"List friend requests failed: {e}")
|
|
187
|
+
|
|
188
|
+
async def _sync_friends(self):
|
|
189
|
+
"""Sync friends from server into local state."""
|
|
190
|
+
try:
|
|
191
|
+
friends = await self.server.list_friends()
|
|
192
|
+
logger.info(f"Synced {len(friends)} friends from server")
|
|
193
|
+
except Exception as e:
|
|
194
|
+
logger.warning(f"Friends sync failed: {e}")
|
|
195
|
+
|
|
196
|
+
# ── Message Dispatch ────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
async def _on_inbound_message(self, msg: dict):
|
|
199
|
+
"""Handle inbound AICQ message and forward to Hermes gateway."""
|
|
200
|
+
from_id = msg.get("from_id")
|
|
201
|
+
content = msg.get("content", "")
|
|
202
|
+
is_group = msg.get("is_group", False)
|
|
203
|
+
|
|
204
|
+
# Skip self messages
|
|
205
|
+
if from_id == self.server.server_account_id:
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Skip empty
|
|
209
|
+
if not content or not content.strip():
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
logger.info(f"Inbound message from {from_id}: {str(content)[:80]}")
|
|
213
|
+
|
|
214
|
+
# Forward to Hermes via the registered message handler
|
|
215
|
+
if self._message_handler:
|
|
216
|
+
try:
|
|
217
|
+
event = AicqMessageEvent(
|
|
218
|
+
chat_id=from_id if not is_group else msg.get("to_id"),
|
|
219
|
+
content=content,
|
|
220
|
+
sender_id=from_id,
|
|
221
|
+
is_group=is_group,
|
|
222
|
+
msg_type=msg.get("type", "text"),
|
|
223
|
+
raw=msg,
|
|
224
|
+
)
|
|
225
|
+
await self._message_handler(event)
|
|
226
|
+
except Exception as e:
|
|
227
|
+
logger.error(f"Message handler error: {e}")
|
|
228
|
+
|
|
229
|
+
def set_message_handler(self, handler):
|
|
230
|
+
"""Set the Hermes message handler (called by gateway to receive messages)."""
|
|
231
|
+
self._message_handler = handler
|
|
232
|
+
|
|
233
|
+
async def _fetch_initial_unread(self):
|
|
234
|
+
"""Fetch unread messages from all friends on startup."""
|
|
235
|
+
try:
|
|
236
|
+
friends = await self.server.list_friends()
|
|
237
|
+
for f in friends:
|
|
238
|
+
fid = f.get("id") or f.get("friend_id")
|
|
239
|
+
if fid:
|
|
240
|
+
await self.chat._fetch_unread(fid)
|
|
241
|
+
logger.info(f"Fetched initial unread from {len(friends)} friends")
|
|
242
|
+
except Exception as e:
|
|
243
|
+
logger.warning(f"Initial unread fetch failed: {e}")
|
|
244
|
+
|
|
245
|
+
# ── Tool Calling Support ────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
async def aicq_status(self) -> dict:
|
|
248
|
+
"""Get AICQ plugin status."""
|
|
249
|
+
return {
|
|
250
|
+
"connected": self._connected,
|
|
251
|
+
"server_url": self.server_url,
|
|
252
|
+
"agent_id": self.agent_id,
|
|
253
|
+
"server_account_id": self.server.server_account_id,
|
|
254
|
+
"master_bound": self._master_bound,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async def aicq_friends_list(self) -> list:
|
|
258
|
+
"""List all friends."""
|
|
259
|
+
return await self.server.list_friends()
|
|
260
|
+
|
|
261
|
+
async def aicq_friends_add(self, aicq_number: str) -> dict:
|
|
262
|
+
"""Add a friend by AICQ number."""
|
|
263
|
+
return await self.server.add_friend_by_number(aicq_number)
|
|
264
|
+
|
|
265
|
+
async def aicq_chat_send(self, target_id: str, content: str, msg_type: str = "text") -> dict:
|
|
266
|
+
"""Send a chat message to a specific user."""
|
|
267
|
+
success = await self.chat.send_message(target_id, content, msg_type)
|
|
268
|
+
return {"success": success}
|
|
269
|
+
|
|
270
|
+
async def aicq_chat_history(self, friend_id: str, limit: int = 50) -> dict:
|
|
271
|
+
"""Get chat history with a friend."""
|
|
272
|
+
return await self.server.get_conversation(friend_id, limit)
|
|
273
|
+
|
|
274
|
+
async def aicq_chat_send_file(self, target_id: str, file_path: str) -> dict:
|
|
275
|
+
"""Send a file to a friend."""
|
|
276
|
+
success = await self.chat.send_file(target_id, file_path)
|
|
277
|
+
return {"success": success}
|
|
278
|
+
|
|
279
|
+
async def aicq_accept_friend_request(self, request_id: str) -> dict:
|
|
280
|
+
"""Accept a friend request."""
|
|
281
|
+
return await self.server.accept_friend_request(request_id)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
class AicqMessageEvent:
|
|
285
|
+
"""Normalized message event for Hermes gateway."""
|
|
286
|
+
|
|
287
|
+
def __init__(self, chat_id: str, content: str, sender_id: str,
|
|
288
|
+
is_group: bool = False, msg_type: str = "text", raw: dict = None):
|
|
289
|
+
self.chat_id = chat_id
|
|
290
|
+
self.content = content
|
|
291
|
+
self.sender_id = sender_id
|
|
292
|
+
self.is_group = is_group
|
|
293
|
+
self.msg_type = msg_type
|
|
294
|
+
self.raw = raw or {}
|
|
295
|
+
|
|
296
|
+
@property
|
|
297
|
+
def text(self) -> str:
|
|
298
|
+
return self.content
|
|
299
|
+
|
|
300
|
+
def to_dict(self) -> dict:
|
|
301
|
+
return {
|
|
302
|
+
"chat_id": self.chat_id,
|
|
303
|
+
"content": self.content,
|
|
304
|
+
"sender_id": self.sender_id,
|
|
305
|
+
"is_group": self.is_group,
|
|
306
|
+
"msg_type": self.msg_type,
|
|
307
|
+
}
|