@ezshine/waifusmy 1.0.0
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.
- package/README.md +163 -0
- package/index.ts +21 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +24 -0
- package/src/channel.ts +469 -0
- package/src/types.ts +95 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# OpenClaw Channel Plugin - WaifusMy
|
|
2
|
+
|
|
3
|
+
[OpenClaw](https://github.com/openclaw/openclaw) Channel Plugin for connecting to WaifusMy desktop companion app.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This plugin enables OpenClaw to connect to the WaifusMy desktop application via a WebSocket relay server, allowing the AI agent to send and receive messages through the WaifusMy companion.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
OpenClaw Agent ◄──Gateway──► WaifusMy Plugin (this project)
|
|
11
|
+
│
|
|
12
|
+
WSS (outbound connection)
|
|
13
|
+
▼
|
|
14
|
+
Relay Server (wss://api.waifus.my)
|
|
15
|
+
│
|
|
16
|
+
WSS
|
|
17
|
+
▼
|
|
18
|
+
Tauri Desktop App (user's computer)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
cd ~/.openclaw/extensions/
|
|
25
|
+
git clone https://github.com/ezshine/clawplugin-waifusmy.git
|
|
26
|
+
cd clawplugin-waifusmy
|
|
27
|
+
npm install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Configuration
|
|
31
|
+
|
|
32
|
+
Add the following to your OpenClaw configuration file (`~/.openclaw/config.yaml` or similar):
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
channels:
|
|
36
|
+
waifusmy:
|
|
37
|
+
enabled: true
|
|
38
|
+
api_key: "waifu_key_xxxxxx" # Your WaifusMy API key
|
|
39
|
+
relay_url: "wss://waifus-relay.ezshine.workers.dev/ws" # Optional, has default
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Features
|
|
43
|
+
|
|
44
|
+
- **WebSocket Connection**: Maintains persistent connection to WaifusMy relay server
|
|
45
|
+
- **Automatic Reconnection**: Exponential on backoff reconnection disconnect
|
|
46
|
+
- **Heartbeat**: Regular ping/pong to keep connection alive
|
|
47
|
+
- **Message Forwarding**:
|
|
48
|
+
- Receives messages from WaifusMy desktop app
|
|
49
|
+
- Sends AI responses back to the app
|
|
50
|
+
- **Simple DM**: Direct message communication (no group support yet)
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
- OpenClaw instance
|
|
55
|
+
- WaifusMy desktop application with API key
|
|
56
|
+
- Network access to `wss://api.waifus.my`
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
### Project Structure
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
openclaw-channel-waifusmy/
|
|
64
|
+
├── package.json # NPM package configuration
|
|
65
|
+
├── tsconfig.json # TypeScript configuration
|
|
66
|
+
├── index.ts # Plugin entry point
|
|
67
|
+
├── src/
|
|
68
|
+
│ ├── channel.ts # Main channel implementation
|
|
69
|
+
│ └── types.ts # TypeScript type definitions
|
|
70
|
+
└── README.md # This file
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Build
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm run build
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Testing
|
|
80
|
+
|
|
81
|
+
The plugin can be tested using a WebSocket client to simulate the relay server:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
# Using wscat
|
|
85
|
+
npm install -g wscat
|
|
86
|
+
wscat -c wss://api.waifus.my/api/channel/ws
|
|
87
|
+
|
|
88
|
+
# Then send auth message:
|
|
89
|
+
{"type": "auth", "api_key": "your_key", "role": "plugin"}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## WebSocket Protocol
|
|
93
|
+
|
|
94
|
+
### Outbound (Plugin → Server)
|
|
95
|
+
|
|
96
|
+
**Authentication:**
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"type": "auth",
|
|
100
|
+
"api_key": "waifu_key_xxx",
|
|
101
|
+
"role": "app"
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Send Message:**
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"type": "message",
|
|
109
|
+
"id": "uuid",
|
|
110
|
+
"text": "AI response",
|
|
111
|
+
"ts": 1739577601000
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Heartbeat:**
|
|
116
|
+
```json
|
|
117
|
+
{ "type": "ping" }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Inbound (Server → Plugin)
|
|
121
|
+
|
|
122
|
+
**Auth Success:**
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"type": "auth_ok",
|
|
126
|
+
"peer_online": true
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Auth Error:**
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"type": "auth_error",
|
|
134
|
+
"error": "Invalid API key"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**User Message:**
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"type": "message",
|
|
142
|
+
"id": "msg_uuid",
|
|
143
|
+
"text": "User's message",
|
|
144
|
+
"ts": 1739577600000
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Peer Status:**
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"type": "peer_status",
|
|
152
|
+
"online": true/false
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Heartbeat Response:**
|
|
157
|
+
```json
|
|
158
|
+
{ "type": "pong" }
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WaifusMy Channel Plugin Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Registers the WaifusMy channel plugin with OpenClaw.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
8
|
+
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
9
|
+
import { waifusmyPlugin } from "./src/channel.js";
|
|
10
|
+
|
|
11
|
+
const plugin = {
|
|
12
|
+
id: "waifusmy",
|
|
13
|
+
name: "WaifusMy",
|
|
14
|
+
description: "OpenClaw channel plugin for WaifusMy desktop companion app",
|
|
15
|
+
configSchema: emptyPluginConfigSchema(),
|
|
16
|
+
register(api: OpenClawPluginApi) {
|
|
17
|
+
api.registerChannel({ plugin: waifusmyPlugin as ChannelPlugin });
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ezshine/waifusmy",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenClaw channel plugin for WaifusMy desktop companion app",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.ts",
|
|
10
|
+
"src",
|
|
11
|
+
"openclaw.plugin.json"
|
|
12
|
+
],
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"typescript": "^5.0.0"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"openclaw": ">=2026.0.0"
|
|
18
|
+
},
|
|
19
|
+
"openclaw": {
|
|
20
|
+
"extensions": [
|
|
21
|
+
"./index.ts"
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/channel.ts
ADDED
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WaifusMy Channel Implementation
|
|
3
|
+
*
|
|
4
|
+
* Connects OpenClaw to WaifusMy desktop companion app via WebSocket relay server.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ChannelPlugin,
|
|
9
|
+
ChannelMeta,
|
|
10
|
+
ChannelCapabilities,
|
|
11
|
+
ChannelId,
|
|
12
|
+
OpenClawConfig,
|
|
13
|
+
} from "openclaw/plugin-sdk";
|
|
14
|
+
import { getChatChannelMeta, buildChannelConfigSchema, DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk";
|
|
15
|
+
import type {
|
|
16
|
+
WaifusMyConfig,
|
|
17
|
+
WaifusMyInboundMessage,
|
|
18
|
+
WaifusMyOutboundMessage,
|
|
19
|
+
WaifusMyMessage,
|
|
20
|
+
WaifusMyAuth,
|
|
21
|
+
WaifusMyAuthOk,
|
|
22
|
+
WaifusMyAuthError,
|
|
23
|
+
WaifusMyPeerStatus,
|
|
24
|
+
ResolvedWaifusMyAccount,
|
|
25
|
+
} from "./types.js";
|
|
26
|
+
import { WaifusMyConfigSchema } from "./types.js";
|
|
27
|
+
|
|
28
|
+
// Channel metadata
|
|
29
|
+
const meta: ChannelMeta = getChatChannelMeta("waifusmy");
|
|
30
|
+
|
|
31
|
+
// Global runtime reference
|
|
32
|
+
let waifusmyRuntime: WaifusMyRuntime | null = null;
|
|
33
|
+
|
|
34
|
+
// Runtime interface
|
|
35
|
+
interface WaifusMyRuntime {
|
|
36
|
+
account: ResolvedWaifusMyAccount | null;
|
|
37
|
+
ws: WebSocket | null;
|
|
38
|
+
reconnectAttempts: number;
|
|
39
|
+
maxReconnectAttempts: number;
|
|
40
|
+
reconnectDelay: number;
|
|
41
|
+
maxReconnectDelay: number;
|
|
42
|
+
connected: boolean;
|
|
43
|
+
authenticated: boolean;
|
|
44
|
+
peerOnline: boolean;
|
|
45
|
+
messageHandler: ((msg: WaifusMyMessage) => void) | null;
|
|
46
|
+
abortSignal: AbortSignal | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Get runtime or create new one
|
|
50
|
+
function getRuntime(): WaifusMyRuntime {
|
|
51
|
+
if (!waifusmyRuntime) {
|
|
52
|
+
waifusmyRuntime = {
|
|
53
|
+
account: null,
|
|
54
|
+
ws: null,
|
|
55
|
+
reconnectAttempts: 0,
|
|
56
|
+
maxReconnectAttempts: 10,
|
|
57
|
+
reconnectDelay: 1000,
|
|
58
|
+
maxReconnectDelay: 30000,
|
|
59
|
+
connected: false,
|
|
60
|
+
authenticated: false,
|
|
61
|
+
peerOnline: false,
|
|
62
|
+
messageHandler: null,
|
|
63
|
+
abortSignal: null,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return waifusmyRuntime;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Reset runtime
|
|
70
|
+
function resetRuntime(): void {
|
|
71
|
+
const runtime = getRuntime();
|
|
72
|
+
if (runtime.ws) {
|
|
73
|
+
runtime.ws.close();
|
|
74
|
+
runtime.ws = null;
|
|
75
|
+
}
|
|
76
|
+
runtime.connected = false;
|
|
77
|
+
runtime.authenticated = false;
|
|
78
|
+
runtime.reconnectAttempts = 0;
|
|
79
|
+
runtime.reconnectDelay = 1000;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Set message handler (called from gateway)
|
|
83
|
+
export function setWaifusMyMessageHandler(
|
|
84
|
+
handler: (msg: WaifusMyMessage) => void
|
|
85
|
+
): void {
|
|
86
|
+
const runtime = getRuntime();
|
|
87
|
+
runtime.messageHandler = handler;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Set abort signal
|
|
91
|
+
export function setWaifusMyAbortSignal(signal: AbortSignal): void {
|
|
92
|
+
const runtime = getRuntime();
|
|
93
|
+
runtime.abortSignal = signal;
|
|
94
|
+
signal.addEventListener("abort", () => {
|
|
95
|
+
console.log("[waifusmy] Abort signal received, closing connection");
|
|
96
|
+
resetRuntime();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// WebSocket connection
|
|
101
|
+
function connectWebSocket(account: ResolvedWaifusMyAccount): Promise<void> {
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
const runtime = getRuntime();
|
|
104
|
+
runtime.account = account;
|
|
105
|
+
|
|
106
|
+
console.log(`[waifusmy] Connecting to ${account.config.relay_url}`);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
runtime.ws = new WebSocket(account.config.relay_url);
|
|
110
|
+
|
|
111
|
+
runtime.ws.onopen = () => {
|
|
112
|
+
console.log("[waifusmy] WebSocket connected");
|
|
113
|
+
runtime.connected = true;
|
|
114
|
+
runtime.reconnectAttempts = 0;
|
|
115
|
+
runtime.reconnectDelay = 1000;
|
|
116
|
+
|
|
117
|
+
// Send authentication
|
|
118
|
+
const authMsg: WaifusMyAuth = {
|
|
119
|
+
type: "auth",
|
|
120
|
+
api_key: account.config.api_key,
|
|
121
|
+
role: "app",
|
|
122
|
+
};
|
|
123
|
+
runtime.ws?.send(JSON.stringify(authMsg));
|
|
124
|
+
console.log("[waifusmy] Sent auth message");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
runtime.ws.onmessage = (event) => {
|
|
128
|
+
try {
|
|
129
|
+
const data = JSON.parse(event.data) as WaifusMyInboundMessage;
|
|
130
|
+
handleInboundMessage(data);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error("[waifusmy] Failed to parse message:", err);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
runtime.ws.onerror = (error) => {
|
|
137
|
+
console.error("[waifusmy] WebSocket error:", error);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
runtime.ws.onclose = () => {
|
|
141
|
+
console.log("[waifusmy] WebSocket closed");
|
|
142
|
+
runtime.connected = false;
|
|
143
|
+
runtime.authenticated = false;
|
|
144
|
+
|
|
145
|
+
// Attempt reconnection if not aborted
|
|
146
|
+
if (!runtime.abortSignal?.aborted) {
|
|
147
|
+
attemptReconnect(account);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
resolve();
|
|
152
|
+
} catch (err) {
|
|
153
|
+
reject(err);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle inbound messages
|
|
159
|
+
function handleInboundMessage(msg: WaifusMyInboundMessage): void {
|
|
160
|
+
const runtime = getRuntime();
|
|
161
|
+
|
|
162
|
+
switch (msg.type) {
|
|
163
|
+
case "auth_ok":
|
|
164
|
+
console.log("[waifusmy] Authentication successful, peer online:", msg.peer_online);
|
|
165
|
+
runtime.authenticated = true;
|
|
166
|
+
runtime.peerOnline = msg.peer_online;
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case "auth_error":
|
|
170
|
+
console.error("[waifusmy] Authentication failed:", msg.error);
|
|
171
|
+
runtime.authenticated = false;
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case "message":
|
|
175
|
+
console.log("[waifusmy] Received message:", msg.id, msg.text.substring(0, 50));
|
|
176
|
+
// Forward to message handler for gateway processing
|
|
177
|
+
if (runtime.messageHandler) {
|
|
178
|
+
runtime.messageHandler(msg);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case "peer_status":
|
|
183
|
+
console.log("[waifusmy] Peer status changed, online:", msg.online);
|
|
184
|
+
runtime.peerOnline = msg.online;
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case "pong":
|
|
188
|
+
// Heartbeat response received
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
default:
|
|
192
|
+
console.log("[waifusmy] Unknown message type:", (msg as any).type);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Exponential backoff reconnection
|
|
197
|
+
function attemptReconnect(account: ResolvedWaifusMyAccount): void {
|
|
198
|
+
const runtime = getRuntime();
|
|
199
|
+
|
|
200
|
+
if (runtime.reconnectAttempts >= runtime.maxReconnectAttempts) {
|
|
201
|
+
console.error("[waifusmy] Max reconnection attempts reached");
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
runtime.reconnectAttempts++;
|
|
206
|
+
const delay = Math.min(
|
|
207
|
+
runtime.reconnectDelay * Math.pow(2, runtime.reconnectAttempts - 1),
|
|
208
|
+
runtime.maxReconnectDelay
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
console.log(
|
|
212
|
+
`[waifusmy] Reconnecting in ${delay}ms (attempt ${runtime.reconnectAttempts}/${runtime.maxReconnectAttempts})`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
if (!runtime.abortSignal?.aborted) {
|
|
217
|
+
connectWebSocket(account).catch((err) => {
|
|
218
|
+
console.error("[waifusmy] Reconnection failed:", err);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}, delay);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Send outbound message
|
|
225
|
+
export async function sendWaifusMyMessage(text: string): Promise<{ channel: string; messageId: string }> {
|
|
226
|
+
const runtime = getRuntime();
|
|
227
|
+
|
|
228
|
+
if (!runtime.ws || !runtime.connected || !runtime.authenticated) {
|
|
229
|
+
throw new Error("WaifusMy not connected or authenticated");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const message: WaifusMyMessage = {
|
|
233
|
+
type: "message",
|
|
234
|
+
id: crypto.randomUUID(),
|
|
235
|
+
text,
|
|
236
|
+
ts: Date.now(),
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
runtime.ws.send(JSON.stringify(message));
|
|
240
|
+
console.log("[waifusmy] Sent message:", message.id);
|
|
241
|
+
|
|
242
|
+
return { channel: "waifusmy", messageId: message.id };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Start heartbeat ping
|
|
246
|
+
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
|
|
247
|
+
|
|
248
|
+
function startHeartbeat(): void {
|
|
249
|
+
if (heartbeatInterval) {
|
|
250
|
+
clearInterval(heartbeatInterval);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
heartbeatInterval = setInterval(() => {
|
|
254
|
+
const runtime = getRuntime();
|
|
255
|
+
if (runtime.ws && runtime.connected) {
|
|
256
|
+
runtime.ws.send(JSON.stringify({ type: "ping" }));
|
|
257
|
+
}
|
|
258
|
+
}, 30000); // Ping every 30 seconds
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function stopHeartbeat(): void {
|
|
262
|
+
if (heartbeatInterval) {
|
|
263
|
+
clearInterval(heartbeatInterval);
|
|
264
|
+
heartbeatInterval = null;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// The channel plugin
|
|
269
|
+
export const waifusmyPlugin: ChannelPlugin<ResolvedWaifusMyAccount> = {
|
|
270
|
+
id: "waifusmy",
|
|
271
|
+
meta: {
|
|
272
|
+
...meta,
|
|
273
|
+
id: "waifusmy" as ChannelId,
|
|
274
|
+
label: "WaifusMy",
|
|
275
|
+
selectionLabel: "WaifusMy Desktop Companion",
|
|
276
|
+
detailLabel: "WaifusMy",
|
|
277
|
+
docsPath: "/channels/waifusmy",
|
|
278
|
+
docsLabel: "waifusmy",
|
|
279
|
+
blurb: "Connect to WaifusMy desktop companion app",
|
|
280
|
+
systemImage: "desktopcomputer",
|
|
281
|
+
quickstartAllowFrom: false,
|
|
282
|
+
},
|
|
283
|
+
capabilities: {
|
|
284
|
+
chatTypes: ["direct"],
|
|
285
|
+
reactions: false,
|
|
286
|
+
threads: false,
|
|
287
|
+
media: false,
|
|
288
|
+
nativeCommands: false,
|
|
289
|
+
blockStreaming: true,
|
|
290
|
+
},
|
|
291
|
+
reload: { configPrefixes: ["channels.waifusmy"] },
|
|
292
|
+
configSchema: buildChannelConfigSchema(WaifusMyConfigSchema),
|
|
293
|
+
config: {
|
|
294
|
+
listAccountIds: (_cfg) => [DEFAULT_ACCOUNT_ID],
|
|
295
|
+
resolveAccount: (cfg, accountId) => {
|
|
296
|
+
const waifusmyConfig = cfg.channels?.waifusmy as WaifusMyConfig | undefined;
|
|
297
|
+
const resolvedId = accountId ?? DEFAULT_ACCOUNT_ID;
|
|
298
|
+
|
|
299
|
+
if (!waifusmyConfig) {
|
|
300
|
+
return {
|
|
301
|
+
accountId: resolvedId,
|
|
302
|
+
name: "WaifusMy",
|
|
303
|
+
enabled: false,
|
|
304
|
+
config: { enabled: false, api_key: "", relay_url: "wss://waifus-relay.ezshine.workers.dev/ws" },
|
|
305
|
+
apiKey: "",
|
|
306
|
+
relayUrl: "wss://waifus-relay.ezshine.workers.dev/ws",
|
|
307
|
+
tokenSource: "none" as const,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
accountId: resolvedId,
|
|
313
|
+
name: "WaifusMy",
|
|
314
|
+
enabled: waifusmyConfig.enabled ?? true,
|
|
315
|
+
config: waifusmyConfig,
|
|
316
|
+
apiKey: waifusmyConfig.api_key ?? "",
|
|
317
|
+
relayUrl: waifusmyConfig.relay_url ?? "wss://waifus-relay.ezshine.workers.dev/ws",
|
|
318
|
+
tokenSource: waifusmyConfig.api_key ? ("config" as const) : ("none" as const),
|
|
319
|
+
};
|
|
320
|
+
},
|
|
321
|
+
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
322
|
+
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
|
323
|
+
const current = (cfg.channels?.waifusmy ?? {}) as Partial<WaifusMyConfig>;
|
|
324
|
+
return {
|
|
325
|
+
...cfg,
|
|
326
|
+
channels: {
|
|
327
|
+
...cfg.channels,
|
|
328
|
+
waifusmy: {
|
|
329
|
+
...current,
|
|
330
|
+
enabled,
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
deleteAccount: ({ cfg }) => {
|
|
336
|
+
const { waifusmy: _waifusmy, ...restChannels } = cfg.channels ?? {};
|
|
337
|
+
return {
|
|
338
|
+
...cfg,
|
|
339
|
+
channels: restChannels,
|
|
340
|
+
};
|
|
341
|
+
},
|
|
342
|
+
isConfigured: (account) => {
|
|
343
|
+
return Boolean(account.config.api_key?.trim());
|
|
344
|
+
},
|
|
345
|
+
describeAccount: (account) => ({
|
|
346
|
+
accountId: account.accountId,
|
|
347
|
+
name: account.name,
|
|
348
|
+
enabled: account.enabled,
|
|
349
|
+
configured: Boolean(account.config.api_key?.trim()),
|
|
350
|
+
tokenSource: account.tokenSource,
|
|
351
|
+
}),
|
|
352
|
+
resolveAllowFrom: () => [],
|
|
353
|
+
formatAllowFrom: ({ allowFrom }) => allowFrom,
|
|
354
|
+
},
|
|
355
|
+
security: {
|
|
356
|
+
resolveDmPolicy: () => ({
|
|
357
|
+
policy: "open" as const,
|
|
358
|
+
allowFrom: [],
|
|
359
|
+
policyPath: "channels.waifusmy.dmPolicy",
|
|
360
|
+
allowFromPath: "channels.waifusmy.",
|
|
361
|
+
approveHint: undefined,
|
|
362
|
+
normalizeEntry: (raw) => raw,
|
|
363
|
+
}),
|
|
364
|
+
collectWarnings: () => [],
|
|
365
|
+
},
|
|
366
|
+
outbound: {
|
|
367
|
+
deliveryMode: "direct",
|
|
368
|
+
sendText: async ({ text }) => {
|
|
369
|
+
return sendWaifusMyMessage(text);
|
|
370
|
+
},
|
|
371
|
+
},
|
|
372
|
+
status: {
|
|
373
|
+
defaultRuntime: {
|
|
374
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
375
|
+
running: false,
|
|
376
|
+
lastStartAt: null,
|
|
377
|
+
lastStopAt: null,
|
|
378
|
+
lastError: null,
|
|
379
|
+
},
|
|
380
|
+
buildChannelSummary: ({ snapshot }) => ({
|
|
381
|
+
configured: snapshot.configured ?? false,
|
|
382
|
+
running: snapshot.running ?? false,
|
|
383
|
+
connected: snapshot.runtime?.connected ?? false,
|
|
384
|
+
authenticated: snapshot.runtime?.authenticated ?? false,
|
|
385
|
+
peerOnline: snapshot.runtime?.peerOnline ?? false,
|
|
386
|
+
lastStartAt: snapshot.lastStartAt ?? null,
|
|
387
|
+
lastStopAt: snapshot.lastStopAt ?? null,
|
|
388
|
+
lastError: snapshot.lastError ?? null,
|
|
389
|
+
}),
|
|
390
|
+
buildAccountSnapshot: ({ account, runtime }) => ({
|
|
391
|
+
accountId: account.accountId,
|
|
392
|
+
name: account.name,
|
|
393
|
+
enabled: account.enabled,
|
|
394
|
+
configured: Boolean(account.config.api_key?.trim()),
|
|
395
|
+
tokenSource: account.tokenSource,
|
|
396
|
+
running: runtime?.running ?? false,
|
|
397
|
+
lastStartAt: runtime?.lastStartAt ?? null,
|
|
398
|
+
lastStopAt: runtime?.lastStopAt ?? null,
|
|
399
|
+
lastError: runtime?.lastError ?? null,
|
|
400
|
+
runtime: {
|
|
401
|
+
connected: waifusmyRuntime?.connected ?? false,
|
|
402
|
+
authenticated: waifusmyRuntime?.authenticated ?? false,
|
|
403
|
+
peerOnline: waifusmyRuntime?.peerOnline ?? false,
|
|
404
|
+
},
|
|
405
|
+
}),
|
|
406
|
+
},
|
|
407
|
+
gateway: {
|
|
408
|
+
startAccount: async (ctx) => {
|
|
409
|
+
const account = ctx.account;
|
|
410
|
+
|
|
411
|
+
console.log(`[waifusmy] Starting gateway for account: ${account.accountId}`);
|
|
412
|
+
|
|
413
|
+
if (!account.config.api_key?.trim()) {
|
|
414
|
+
throw new Error("WaifusMy API key not configured");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Set abort signal
|
|
418
|
+
setWaifusMyAbortSignal(ctx.abortSignal);
|
|
419
|
+
|
|
420
|
+
// Set message handler to forward to gateway
|
|
421
|
+
setWaifusMyMessageHandler((msg) => {
|
|
422
|
+
// Create inbound message for gateway
|
|
423
|
+
ctx.log?.info(`[waifusmy] Processing inbound message: ${msg.id}`);
|
|
424
|
+
|
|
425
|
+
// The message handler should inject into gateway's message pipeline
|
|
426
|
+
// This is typically done via runtime.handleInboundMessage or similar
|
|
427
|
+
if (ctx.runtime?.onInboundMessage) {
|
|
428
|
+
ctx.runtime.onInboundMessage({
|
|
429
|
+
channel: "waifusmy",
|
|
430
|
+
messageId: msg.id,
|
|
431
|
+
text: msg.text,
|
|
432
|
+
senderId: "user",
|
|
433
|
+
timestamp: msg.ts,
|
|
434
|
+
chatType: "direct",
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Connect to WebSocket
|
|
440
|
+
await connectWebSocket(account);
|
|
441
|
+
startHeartbeat();
|
|
442
|
+
|
|
443
|
+
ctx.log?.info("[waifusmy] Gateway started successfully");
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
running: true,
|
|
447
|
+
mode: "websocket",
|
|
448
|
+
};
|
|
449
|
+
},
|
|
450
|
+
logoutAccount: async ({ accountId, cfg }) => {
|
|
451
|
+
stopHeartbeat();
|
|
452
|
+
resetRuntime();
|
|
453
|
+
|
|
454
|
+
const { waifusmy: _waifusmy, ...restChannels } = cfg.channels ?? {};
|
|
455
|
+
const newCfg = {
|
|
456
|
+
...cfg,
|
|
457
|
+
channels: restChannels,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
cleared: true,
|
|
462
|
+
loggedOut: true,
|
|
463
|
+
};
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// Export types for external use
|
|
469
|
+
export type { WaifusMyConfig, WaifusMyMessage };
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WaifusMy Channel Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { z } from "zod";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
// WebSocket message types from the relay server
|
|
9
|
+
export const WaifusMyAuthSchema = z.object({
|
|
10
|
+
type: z.literal("auth"),
|
|
11
|
+
api_key: z.string(),
|
|
12
|
+
role: z.literal("plugin"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const WaifusMyAuthOkSchema = z.object({
|
|
16
|
+
type: z.literal("auth_ok"),
|
|
17
|
+
peer_online: z.boolean(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const WaifusMyAuthErrorSchema = z.object({
|
|
21
|
+
type: z.literal("auth_error"),
|
|
22
|
+
error: z.string(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const WaifusMyMessageSchema = z.object({
|
|
26
|
+
type: z.literal("message"),
|
|
27
|
+
id: z.string(),
|
|
28
|
+
text: z.string(),
|
|
29
|
+
ts: z.number(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const WaifusMyPeerStatusSchema = z.object({
|
|
33
|
+
type: z.literal("peer_status"),
|
|
34
|
+
online: z.boolean(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const WaifusMyPingSchema = z.object({
|
|
38
|
+
type: z.literal("ping"),
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
export const WaifusMyPongSchema = z.object({
|
|
42
|
+
type: z.literal("pong"),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type WaifusMyAuth = z.infer<typeof WaifusMyAuthSchema>;
|
|
46
|
+
export type WaifusMyAuthOk = z.infer<typeof WaifusMyAuthOkSchema>;
|
|
47
|
+
export type WaifusMyAuthError = z.infer<typeof WaifusMyAuthErrorSchema>;
|
|
48
|
+
export type WaifusMyMessage = z.infer<typeof WaifusMyMessageSchema>;
|
|
49
|
+
export type WaifusMyPeerStatus = z.infer<typeof WaifusMyPeerStatusSchema>;
|
|
50
|
+
export type WaifusMyPing = z.infer<typeof WaifusMyPingSchema>;
|
|
51
|
+
export type WaifusMyPong = z.infer<typeof WaifusMyPongSchema>;
|
|
52
|
+
|
|
53
|
+
export type WaifusMyInboundMessage =
|
|
54
|
+
| WaifusMyAuthOk
|
|
55
|
+
| WaifusMyAuthError
|
|
56
|
+
| WaifusMyMessage
|
|
57
|
+
| WaifusMyPeerStatus
|
|
58
|
+
| WaifusMyPong;
|
|
59
|
+
|
|
60
|
+
export type WaifusMyOutboundMessage =
|
|
61
|
+
| WaifusMyAuth
|
|
62
|
+
| WaifusMyMessage
|
|
63
|
+
| WaifusMyPing;
|
|
64
|
+
|
|
65
|
+
// Configuration schema
|
|
66
|
+
export const WaifusMyConfigSchema = z.object({
|
|
67
|
+
enabled: z.boolean().default(true),
|
|
68
|
+
api_key: z.string().min(1, "API key is required"),
|
|
69
|
+
relay_url: z.string().url().default("wss://waifus-relay.ezshine.workers.dev/ws"),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export type WaifusMyConfig = z.infer<typeof WaifusMyConfigSchema>;
|
|
73
|
+
|
|
74
|
+
// Resolved account type
|
|
75
|
+
export interface ResolvedWaifusMyAccount {
|
|
76
|
+
accountId: string;
|
|
77
|
+
name: string;
|
|
78
|
+
enabled: boolean;
|
|
79
|
+
config: WaifusMyConfig;
|
|
80
|
+
apiKey: string;
|
|
81
|
+
relayUrl: string;
|
|
82
|
+
tokenSource: "config" | "none";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Runtime state
|
|
86
|
+
export interface WaifusMyRuntime {
|
|
87
|
+
ws: WebSocket | null;
|
|
88
|
+
reconnectAttempts: number;
|
|
89
|
+
maxReconnectAttempts: number;
|
|
90
|
+
reconnectDelay: number;
|
|
91
|
+
maxReconnectDelay: number;
|
|
92
|
+
connected: boolean;
|
|
93
|
+
authenticated: boolean;
|
|
94
|
+
peerOnline: boolean;
|
|
95
|
+
}
|