wopr-plugin-router 0.3.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 +235 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +118 -0
- package/dist/ui.d.ts +29 -0
- package/dist/ui.js +121 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# WOPR Router Plugin
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://github.com/TSavo/wopr)
|
|
5
|
+
|
|
6
|
+
A middleware plugin for [WOPR](https://github.com/TSavo/wopr) that routes messages between channels and sessions. Fan out incoming messages to multiple sessions or forward outgoing responses to specific channels.
|
|
7
|
+
|
|
8
|
+
> Part of the [WOPR](https://github.com/TSavo/wopr) ecosystem - Self-sovereign AI session management over P2P.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- **Incoming Routes**: Fan out messages from one session to multiple target sessions
|
|
13
|
+
- **Outgoing Routes**: Forward responses to specific channel types or IDs
|
|
14
|
+
- **Web UI**: Built-in configuration panel integrated into WOPR settings
|
|
15
|
+
- **Hot Reload**: Configuration changes apply immediately without restart
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
Place the plugin in your WOPR plugins directory or install via npm:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install wopr-plugin-router
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The plugin exports an ES module with the following interface:
|
|
26
|
+
|
|
27
|
+
```javascript
|
|
28
|
+
export default {
|
|
29
|
+
name: "router",
|
|
30
|
+
version: "0.1.0",
|
|
31
|
+
description: "Example routing middleware between channels and sessions",
|
|
32
|
+
init(pluginContext),
|
|
33
|
+
shutdown()
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Configuration
|
|
38
|
+
|
|
39
|
+
### Plugin Configuration
|
|
40
|
+
|
|
41
|
+
Configure routes in the plugin config (stored at `plugins.data.router`):
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{
|
|
45
|
+
"uiPort": 7333,
|
|
46
|
+
"routes": [
|
|
47
|
+
{
|
|
48
|
+
"sourceSession": "support",
|
|
49
|
+
"targetSessions": ["billing", "engineering"],
|
|
50
|
+
"channelType": "discord",
|
|
51
|
+
"channelId": "123456789"
|
|
52
|
+
}
|
|
53
|
+
],
|
|
54
|
+
"outgoingRoutes": [
|
|
55
|
+
{
|
|
56
|
+
"sourceSession": "support",
|
|
57
|
+
"channelType": "discord",
|
|
58
|
+
"channelId": "123456789"
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Configuration Options
|
|
65
|
+
|
|
66
|
+
| Option | Type | Default | Description |
|
|
67
|
+
|--------|------|---------|-------------|
|
|
68
|
+
| `uiPort` | number | `7333` | Port for the plugin's web UI server |
|
|
69
|
+
| `routes` | array | `[]` | Incoming message routing rules |
|
|
70
|
+
| `outgoingRoutes` | array | `[]` | Outgoing response routing rules |
|
|
71
|
+
|
|
72
|
+
### Route Fields
|
|
73
|
+
|
|
74
|
+
**Incoming Routes (`routes`):**
|
|
75
|
+
|
|
76
|
+
| Field | Type | Required | Description |
|
|
77
|
+
|-------|------|----------|-------------|
|
|
78
|
+
| `sourceSession` | string | No | Match messages from this session |
|
|
79
|
+
| `targetSessions` | array | Yes | Forward messages to these sessions |
|
|
80
|
+
| `channelType` | string | No | Match only this channel type (e.g., "discord", "slack") |
|
|
81
|
+
| `channelId` | string | No | Match only this specific channel ID |
|
|
82
|
+
|
|
83
|
+
**Outgoing Routes (`outgoingRoutes`):**
|
|
84
|
+
|
|
85
|
+
| Field | Type | Required | Description |
|
|
86
|
+
|-------|------|----------|-------------|
|
|
87
|
+
| `sourceSession` | string | No | Match responses from this session |
|
|
88
|
+
| `channelType` | string | No | Forward only to channels of this type |
|
|
89
|
+
| `channelId` | string | No | Forward only to this specific channel ID |
|
|
90
|
+
|
|
91
|
+
### CLI Configuration
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
wopr config set plugins.data.router '{
|
|
95
|
+
"uiPort": 7333,
|
|
96
|
+
"routes": [
|
|
97
|
+
{
|
|
98
|
+
"sourceSession": "support",
|
|
99
|
+
"targetSessions": ["billing", "engineering"],
|
|
100
|
+
"channelType": "discord"
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
"outgoingRoutes": [
|
|
104
|
+
{
|
|
105
|
+
"sourceSession": "support",
|
|
106
|
+
"channelType": "discord"
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### API Configuration
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
curl -X PUT http://localhost:7437/config/plugins.data.router \
|
|
116
|
+
-H "Content-Type: application/json" \
|
|
117
|
+
-d '{
|
|
118
|
+
"uiPort": 7333,
|
|
119
|
+
"routes": [
|
|
120
|
+
{
|
|
121
|
+
"sourceSession": "support",
|
|
122
|
+
"targetSessions": ["billing", "engineering"],
|
|
123
|
+
"channelType": "discord"
|
|
124
|
+
}
|
|
125
|
+
],
|
|
126
|
+
"outgoingRoutes": [
|
|
127
|
+
{
|
|
128
|
+
"sourceSession": "support",
|
|
129
|
+
"channelType": "discord"
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
}'
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Web UI
|
|
136
|
+
|
|
137
|
+
The plugin includes a web-based configuration panel that integrates into WOPR's settings interface.
|
|
138
|
+
|
|
139
|
+
- **URL**: `http://127.0.0.1:7333` (or configured `uiPort`)
|
|
140
|
+
- **Location**: Appears in WOPR settings under "Message Router"
|
|
141
|
+
- **Features**: Add/delete routing rules, view current configuration
|
|
142
|
+
|
|
143
|
+
The UI component is built with SolidJS signals for reactive updates.
|
|
144
|
+
|
|
145
|
+
## Behavior
|
|
146
|
+
|
|
147
|
+
### Incoming Message Flow
|
|
148
|
+
|
|
149
|
+
1. Message arrives at a session via a channel
|
|
150
|
+
2. Plugin checks all `routes` for matches
|
|
151
|
+
3. For each matching route, message is injected into `targetSessions`
|
|
152
|
+
4. Original message continues to the source session normally
|
|
153
|
+
|
|
154
|
+
**Match Logic**: A route matches if ALL specified fields match:
|
|
155
|
+
- `sourceSession` matches the message's session (if specified)
|
|
156
|
+
- `channelType` matches the channel's type (if specified)
|
|
157
|
+
- `channelId` matches the channel's ID (if specified)
|
|
158
|
+
|
|
159
|
+
### Outgoing Response Flow
|
|
160
|
+
|
|
161
|
+
1. Session generates a response
|
|
162
|
+
2. Plugin checks all `outgoingRoutes` for matches
|
|
163
|
+
3. For each matching route, response is sent to channels connected to that session
|
|
164
|
+
4. Channel filtering applies: only channels matching `channelType` and/or `channelId` receive the response
|
|
165
|
+
|
|
166
|
+
## Plugin Context API
|
|
167
|
+
|
|
168
|
+
The plugin uses the following WOPR plugin context methods:
|
|
169
|
+
|
|
170
|
+
| Method | Description |
|
|
171
|
+
|--------|-------------|
|
|
172
|
+
| `ctx.getPluginDir()` | Get the plugin's directory path |
|
|
173
|
+
| `ctx.getConfig()` | Get the plugin's current configuration |
|
|
174
|
+
| `ctx.log.info(msg)` | Log informational messages |
|
|
175
|
+
| `ctx.registerMiddleware(config)` | Register incoming/outgoing middleware |
|
|
176
|
+
| `ctx.registerUiComponent(config)` | Register a UI component in WOPR |
|
|
177
|
+
| `ctx.inject(session, message)` | Inject a message into a session |
|
|
178
|
+
| `ctx.getChannelsForSession(session)` | Get all channel adapters for a session |
|
|
179
|
+
|
|
180
|
+
## Examples
|
|
181
|
+
|
|
182
|
+
### Support Ticket Routing
|
|
183
|
+
|
|
184
|
+
Route customer support messages to both billing and engineering teams:
|
|
185
|
+
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"routes": [
|
|
189
|
+
{
|
|
190
|
+
"sourceSession": "support",
|
|
191
|
+
"targetSessions": ["billing", "engineering"],
|
|
192
|
+
"channelType": "discord"
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Channel-Specific Routing
|
|
199
|
+
|
|
200
|
+
Route messages from a specific Discord channel to multiple sessions:
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"routes": [
|
|
205
|
+
{
|
|
206
|
+
"channelId": "1234567890",
|
|
207
|
+
"targetSessions": ["alerts", "logs", "monitoring"]
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Broadcast Responses
|
|
214
|
+
|
|
215
|
+
Send all responses from a session to all connected Discord channels:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"outgoingRoutes": [
|
|
220
|
+
{
|
|
221
|
+
"sourceSession": "announcements",
|
|
222
|
+
"channelType": "discord"
|
|
223
|
+
}
|
|
224
|
+
]
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Requirements
|
|
229
|
+
|
|
230
|
+
- WOPR >= 1.0.0
|
|
231
|
+
- Node.js (ES modules support)
|
|
232
|
+
|
|
233
|
+
## License
|
|
234
|
+
|
|
235
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WOPR Router Plugin
|
|
3
|
+
*
|
|
4
|
+
* Middleware-driven routing between channels and sessions.
|
|
5
|
+
*/
|
|
6
|
+
interface Route {
|
|
7
|
+
sourceSession?: string;
|
|
8
|
+
targetSessions?: string[];
|
|
9
|
+
channelType?: string;
|
|
10
|
+
channelId?: string;
|
|
11
|
+
}
|
|
12
|
+
interface OutgoingRoute {
|
|
13
|
+
sourceSession?: string;
|
|
14
|
+
channelType?: string;
|
|
15
|
+
channelId?: string;
|
|
16
|
+
}
|
|
17
|
+
interface RouterConfig {
|
|
18
|
+
uiPort?: number;
|
|
19
|
+
routes?: Route[];
|
|
20
|
+
outgoingRoutes?: OutgoingRoute[];
|
|
21
|
+
}
|
|
22
|
+
interface Channel {
|
|
23
|
+
type: string;
|
|
24
|
+
id: string;
|
|
25
|
+
}
|
|
26
|
+
interface ChannelAdapter {
|
|
27
|
+
channel: Channel;
|
|
28
|
+
send(message: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
interface IncomingInput {
|
|
31
|
+
session: string;
|
|
32
|
+
channel?: Channel;
|
|
33
|
+
message: string;
|
|
34
|
+
}
|
|
35
|
+
interface OutgoingOutput {
|
|
36
|
+
session: string;
|
|
37
|
+
response: string;
|
|
38
|
+
}
|
|
39
|
+
interface Logger {
|
|
40
|
+
info(message: string): void;
|
|
41
|
+
warn(message: string): void;
|
|
42
|
+
error(message: string): void;
|
|
43
|
+
}
|
|
44
|
+
interface UiComponentConfig {
|
|
45
|
+
id: string;
|
|
46
|
+
title: string;
|
|
47
|
+
moduleUrl: string;
|
|
48
|
+
slot: string;
|
|
49
|
+
description: string;
|
|
50
|
+
}
|
|
51
|
+
interface PluginContext {
|
|
52
|
+
log: Logger;
|
|
53
|
+
getConfig(): RouterConfig;
|
|
54
|
+
getPluginDir(): string;
|
|
55
|
+
inject(session: string, message: string): Promise<void>;
|
|
56
|
+
getChannelsForSession(session: string): ChannelAdapter[];
|
|
57
|
+
registerMiddleware(middleware: {
|
|
58
|
+
name: string;
|
|
59
|
+
onIncoming?(input: IncomingInput): Promise<string>;
|
|
60
|
+
onOutgoing?(output: OutgoingOutput): Promise<string>;
|
|
61
|
+
}): void;
|
|
62
|
+
registerUiComponent?(config: UiComponentConfig): void;
|
|
63
|
+
}
|
|
64
|
+
declare const _default: {
|
|
65
|
+
name: string;
|
|
66
|
+
version: string;
|
|
67
|
+
description: string;
|
|
68
|
+
init(pluginContext: PluginContext): Promise<void>;
|
|
69
|
+
shutdown(): Promise<void>;
|
|
70
|
+
};
|
|
71
|
+
export default _default;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WOPR Router Plugin
|
|
3
|
+
*
|
|
4
|
+
* Middleware-driven routing between channels and sessions.
|
|
5
|
+
*/
|
|
6
|
+
import http from "http";
|
|
7
|
+
import { createReadStream } from "fs";
|
|
8
|
+
import { extname, join } from "path";
|
|
9
|
+
const CONTENT_TYPES = {
|
|
10
|
+
".js": "application/javascript",
|
|
11
|
+
".css": "text/css",
|
|
12
|
+
".html": "text/html",
|
|
13
|
+
};
|
|
14
|
+
let ctx = null;
|
|
15
|
+
let uiServer = null;
|
|
16
|
+
function startUIServer(port = 7333) {
|
|
17
|
+
const server = http.createServer((req, res) => {
|
|
18
|
+
const url = req.url === "/" ? "/ui.js" : req.url || "/ui.js";
|
|
19
|
+
const filePath = join(ctx.getPluginDir(), "dist", url);
|
|
20
|
+
const ext = extname(filePath).toLowerCase();
|
|
21
|
+
res.setHeader("Content-Type", CONTENT_TYPES[ext] || "application/octet-stream");
|
|
22
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
23
|
+
try {
|
|
24
|
+
const stream = createReadStream(filePath);
|
|
25
|
+
stream.pipe(res);
|
|
26
|
+
stream.on("error", () => {
|
|
27
|
+
res.statusCode = 404;
|
|
28
|
+
res.end("Not found");
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
res.statusCode = 500;
|
|
33
|
+
res.end("Error");
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
server.listen(port, "127.0.0.1", () => {
|
|
37
|
+
ctx?.log.info(`Router UI available at http://127.0.0.1:${port}`);
|
|
38
|
+
});
|
|
39
|
+
return server;
|
|
40
|
+
}
|
|
41
|
+
function matchesRoute(route, input) {
|
|
42
|
+
if (route.sourceSession && route.sourceSession !== input.session)
|
|
43
|
+
return false;
|
|
44
|
+
if (route.channelType && route.channelType !== input.channel?.type)
|
|
45
|
+
return false;
|
|
46
|
+
if (route.channelId && route.channelId !== input.channel?.id)
|
|
47
|
+
return false;
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
async function fanOutToSessions(route, input) {
|
|
51
|
+
const targets = route.targetSessions || [];
|
|
52
|
+
for (const target of targets) {
|
|
53
|
+
if (!target || target === input.session)
|
|
54
|
+
continue;
|
|
55
|
+
await ctx.inject(target, input.message);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function fanOutToChannels(route, output) {
|
|
59
|
+
const channels = ctx.getChannelsForSession(output.session);
|
|
60
|
+
for (const adapter of channels) {
|
|
61
|
+
if (route.channelType && adapter.channel.type !== route.channelType)
|
|
62
|
+
continue;
|
|
63
|
+
if (route.channelId && adapter.channel.id !== route.channelId)
|
|
64
|
+
continue;
|
|
65
|
+
await adapter.send(output.response);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export default {
|
|
69
|
+
name: "router",
|
|
70
|
+
version: "0.2.0",
|
|
71
|
+
description: "Message routing middleware between channels and sessions",
|
|
72
|
+
async init(pluginContext) {
|
|
73
|
+
ctx = pluginContext;
|
|
74
|
+
const config = ctx.getConfig();
|
|
75
|
+
const uiPort = config.uiPort || 7333;
|
|
76
|
+
uiServer = startUIServer(uiPort);
|
|
77
|
+
if (ctx.registerUiComponent) {
|
|
78
|
+
ctx.registerUiComponent({
|
|
79
|
+
id: "router-panel",
|
|
80
|
+
title: "Message Router",
|
|
81
|
+
moduleUrl: `http://127.0.0.1:${uiPort}/ui.js`,
|
|
82
|
+
slot: "settings",
|
|
83
|
+
description: "Configure message routing between sessions",
|
|
84
|
+
});
|
|
85
|
+
ctx.log.info("Registered Router UI component in WOPR settings");
|
|
86
|
+
}
|
|
87
|
+
ctx.registerMiddleware({
|
|
88
|
+
name: "router",
|
|
89
|
+
async onIncoming(input) {
|
|
90
|
+
const config = ctx.getConfig();
|
|
91
|
+
const routes = config.routes || [];
|
|
92
|
+
for (const route of routes) {
|
|
93
|
+
if (!matchesRoute(route, input))
|
|
94
|
+
continue;
|
|
95
|
+
await fanOutToSessions(route, input);
|
|
96
|
+
}
|
|
97
|
+
return input.message;
|
|
98
|
+
},
|
|
99
|
+
async onOutgoing(output) {
|
|
100
|
+
const config = ctx.getConfig();
|
|
101
|
+
const routes = config.outgoingRoutes || [];
|
|
102
|
+
for (const route of routes) {
|
|
103
|
+
if (route.sourceSession && route.sourceSession !== output.session)
|
|
104
|
+
continue;
|
|
105
|
+
await fanOutToChannels(route, output);
|
|
106
|
+
}
|
|
107
|
+
return output.response;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
async shutdown() {
|
|
112
|
+
if (uiServer) {
|
|
113
|
+
ctx?.log.info("Router UI server shutting down...");
|
|
114
|
+
await new Promise((resolve) => uiServer.close(() => resolve()));
|
|
115
|
+
uiServer = null;
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Plugin UI Component for WOPR
|
|
3
|
+
*
|
|
4
|
+
* SolidJS component for managing message routing rules.
|
|
5
|
+
*/
|
|
6
|
+
interface Route {
|
|
7
|
+
sourceSession: string;
|
|
8
|
+
targetSessions: string[];
|
|
9
|
+
channelType?: string;
|
|
10
|
+
}
|
|
11
|
+
interface RouterConfig {
|
|
12
|
+
routes?: Route[];
|
|
13
|
+
outgoingRoutes?: Route[];
|
|
14
|
+
}
|
|
15
|
+
interface PluginConfig {
|
|
16
|
+
plugins?: {
|
|
17
|
+
data?: {
|
|
18
|
+
router?: RouterConfig;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
interface RouterPluginProps {
|
|
23
|
+
api: {
|
|
24
|
+
getConfig(): Promise<PluginConfig>;
|
|
25
|
+
};
|
|
26
|
+
saveConfig(config: RouterConfig): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
export default function RouterPluginUI(props: RouterPluginProps): HTMLDivElement;
|
|
29
|
+
export {};
|
package/dist/ui.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// src/ui.ts
|
|
2
|
+
var { createSignal, onMount } = window.Solid || Solid;
|
|
3
|
+
function RouterPluginUI(props) {
|
|
4
|
+
const [routes, setRoutes] = createSignal([]);
|
|
5
|
+
const [outgoingRoutes, setOutgoingRoutes] = createSignal([]);
|
|
6
|
+
const [newSource, setNewSource] = createSignal("");
|
|
7
|
+
const [newTargets, setNewTargets] = createSignal("");
|
|
8
|
+
const [newChannelType, setNewChannelType] = createSignal("");
|
|
9
|
+
onMount(async () => {
|
|
10
|
+
const config = await props.api.getConfig();
|
|
11
|
+
const routerConfig = config.plugins?.data?.router || {};
|
|
12
|
+
setRoutes(routerConfig.routes || []);
|
|
13
|
+
setOutgoingRoutes(routerConfig.outgoingRoutes || []);
|
|
14
|
+
});
|
|
15
|
+
const handleAddRoute = async () => {
|
|
16
|
+
if (!newSource() || !newTargets())
|
|
17
|
+
return;
|
|
18
|
+
const newRoute = {
|
|
19
|
+
sourceSession: newSource(),
|
|
20
|
+
targetSessions: newTargets().split(",").map((s) => s.trim()).filter(Boolean),
|
|
21
|
+
channelType: newChannelType() || void 0
|
|
22
|
+
};
|
|
23
|
+
const updatedRoutes = [...routes(), newRoute];
|
|
24
|
+
await props.saveConfig({
|
|
25
|
+
routes: updatedRoutes,
|
|
26
|
+
outgoingRoutes: outgoingRoutes()
|
|
27
|
+
});
|
|
28
|
+
setRoutes(updatedRoutes);
|
|
29
|
+
setNewSource("");
|
|
30
|
+
setNewTargets("");
|
|
31
|
+
setNewChannelType("");
|
|
32
|
+
};
|
|
33
|
+
const handleDeleteRoute = async (index) => {
|
|
34
|
+
const updatedRoutes = routes().filter((_, i) => i !== index);
|
|
35
|
+
await props.saveConfig({
|
|
36
|
+
routes: updatedRoutes,
|
|
37
|
+
outgoingRoutes: outgoingRoutes()
|
|
38
|
+
});
|
|
39
|
+
setRoutes(updatedRoutes);
|
|
40
|
+
};
|
|
41
|
+
const container = document.createElement("div");
|
|
42
|
+
container.className = "router-plugin-ui";
|
|
43
|
+
const header = document.createElement("div");
|
|
44
|
+
header.className = "flex items-center justify-between mb-4";
|
|
45
|
+
header.innerHTML = `
|
|
46
|
+
<h3 class="text-lg font-semibold">Message Router</h3>
|
|
47
|
+
<span class="px-2 py-1 rounded text-xs bg-blue-500/20 text-blue-400">
|
|
48
|
+
${routes().length} rules
|
|
49
|
+
</span>
|
|
50
|
+
`;
|
|
51
|
+
container.appendChild(header);
|
|
52
|
+
const routesSection = document.createElement("div");
|
|
53
|
+
routesSection.className = "mb-4";
|
|
54
|
+
const updateRoutesList = () => {
|
|
55
|
+
routesSection.innerHTML = "";
|
|
56
|
+
if (routes().length === 0) {
|
|
57
|
+
routesSection.innerHTML = `
|
|
58
|
+
<div class="text-sm text-wopr-muted p-3 bg-wopr-panel rounded border border-wopr-border">
|
|
59
|
+
No routing rules configured. Messages will not be forwarded.
|
|
60
|
+
</div>
|
|
61
|
+
`;
|
|
62
|
+
} else {
|
|
63
|
+
routes().forEach((route, index) => {
|
|
64
|
+
const item = document.createElement("div");
|
|
65
|
+
item.className = "p-3 bg-wopr-panel rounded border border-wopr-border flex items-center justify-between mb-2";
|
|
66
|
+
item.innerHTML = `
|
|
67
|
+
<div>
|
|
68
|
+
<div class="font-medium">${route.sourceSession} \u2192 ${route.targetSessions.join(", ")}</div>
|
|
69
|
+
${route.channelType ? `<div class="text-sm text-wopr-muted">Channel: ${route.channelType}</div>` : ""}
|
|
70
|
+
</div>
|
|
71
|
+
<button class="delete-route px-3 py-1 bg-red-500/20 text-red-400 rounded text-sm hover:bg-red-500/30">
|
|
72
|
+
Delete
|
|
73
|
+
</button>
|
|
74
|
+
`;
|
|
75
|
+
const deleteBtn = item.querySelector(".delete-route");
|
|
76
|
+
if (deleteBtn) {
|
|
77
|
+
deleteBtn.addEventListener("click", () => handleDeleteRoute(index));
|
|
78
|
+
}
|
|
79
|
+
routesSection.appendChild(item);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
routes(updateRoutesList);
|
|
84
|
+
updateRoutesList();
|
|
85
|
+
container.appendChild(routesSection);
|
|
86
|
+
const formSection = document.createElement("div");
|
|
87
|
+
formSection.className = "p-3 bg-wopr-panel rounded border border-wopr-border";
|
|
88
|
+
formSection.innerHTML = `
|
|
89
|
+
<h4 class="text-sm font-semibold text-wopr-muted uppercase mb-3">Add Route</h4>
|
|
90
|
+
<div class="space-y-2">
|
|
91
|
+
<input type="text" placeholder="Source session" class="source-input w-full bg-wopr-bg border border-wopr-border rounded px-3 py-2 text-sm" />
|
|
92
|
+
<input type="text" placeholder="Target sessions (comma-separated)" class="targets-input w-full bg-wopr-bg border border-wopr-border rounded px-3 py-2 text-sm" />
|
|
93
|
+
<input type="text" placeholder="Channel type (optional)" class="channel-input w-full bg-wopr-bg border border-wopr-border rounded px-3 py-2 text-sm" />
|
|
94
|
+
<button class="add-btn w-full px-4 py-2 bg-wopr-accent text-wopr-bg rounded text-sm font-medium hover:bg-wopr-accent/90">
|
|
95
|
+
Add Route
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
`;
|
|
99
|
+
const sourceInput = formSection.querySelector(".source-input");
|
|
100
|
+
const targetsInput = formSection.querySelector(".targets-input");
|
|
101
|
+
const channelInput = formSection.querySelector(".channel-input");
|
|
102
|
+
sourceInput.addEventListener("input", (e) => setNewSource(e.target.value));
|
|
103
|
+
targetsInput.addEventListener("input", (e) => setNewTargets(e.target.value));
|
|
104
|
+
channelInput.addEventListener("input", (e) => setNewChannelType(e.target.value));
|
|
105
|
+
const addBtn = formSection.querySelector(".add-btn");
|
|
106
|
+
if (addBtn) {
|
|
107
|
+
addBtn.addEventListener("click", handleAddRoute);
|
|
108
|
+
}
|
|
109
|
+
container.appendChild(formSection);
|
|
110
|
+
const infoSection = document.createElement("div");
|
|
111
|
+
infoSection.className = "mt-4 p-3 bg-wopr-panel/50 rounded border border-wopr-border text-sm text-wopr-muted";
|
|
112
|
+
infoSection.innerHTML = `
|
|
113
|
+
<p class="mb-1"><strong>Incoming:</strong> Messages to source session are forwarded to targets.</p>
|
|
114
|
+
<p><strong>Outgoing:</strong> Responses are sent back to originating channel.</p>
|
|
115
|
+
`;
|
|
116
|
+
container.appendChild(infoSection);
|
|
117
|
+
return container;
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
RouterPluginUI as default
|
|
121
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wopr-plugin-router",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Message routing middleware for WOPR",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc && npm run build:ui",
|
|
10
|
+
"build:ui": "esbuild src/ui.ts --bundle --format=esm --outfile=dist/ui.js --target=es2020",
|
|
11
|
+
"dev": "tsc --watch",
|
|
12
|
+
"clean": "rm -rf dist",
|
|
13
|
+
"prepublishOnly": "npm run build"
|
|
14
|
+
},
|
|
15
|
+
"wopr": {
|
|
16
|
+
"type": "plugin",
|
|
17
|
+
"minVersion": "1.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/node": "^22.0.0",
|
|
21
|
+
"esbuild": "^0.20.0",
|
|
22
|
+
"typescript": "^5.3.0"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
]
|
|
27
|
+
}
|