openclaw-channel-openswitchy 0.1.0 → 0.1.2
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/dist/channel.d.ts +2 -1
- package/dist/channel.js +80 -34
- package/dist/channel.test.d.ts +1 -0
- package/dist/channel.test.js +211 -0
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +4 -112
- package/dist/types.js +4 -2
- package/package.json +17 -5
package/dist/channel.d.ts
CHANGED
package/dist/channel.js
CHANGED
|
@@ -47,7 +47,7 @@ async function listenSse(conn) {
|
|
|
47
47
|
signal,
|
|
48
48
|
});
|
|
49
49
|
if (!res.ok || !res.body) {
|
|
50
|
-
|
|
50
|
+
conn.gatewayCtx.log?.error(`SSE connect failed (${res.status}), retrying in 5s`);
|
|
51
51
|
await sleep(5000);
|
|
52
52
|
continue;
|
|
53
53
|
}
|
|
@@ -72,7 +72,7 @@ async function listenSse(conn) {
|
|
|
72
72
|
await handleNewMessage(conn, data);
|
|
73
73
|
}
|
|
74
74
|
catch (err) {
|
|
75
|
-
|
|
75
|
+
conn.gatewayCtx.log?.error(`Failed to parse SSE data: ${err}`);
|
|
76
76
|
}
|
|
77
77
|
currentEvent = "";
|
|
78
78
|
}
|
|
@@ -85,7 +85,7 @@ async function listenSse(conn) {
|
|
|
85
85
|
catch (err) {
|
|
86
86
|
if (signal.aborted)
|
|
87
87
|
return;
|
|
88
|
-
|
|
88
|
+
conn.gatewayCtx.log?.error(`SSE error, reconnecting in 5s: ${err}`);
|
|
89
89
|
await sleep(5000);
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -99,23 +99,69 @@ async function handleNewMessage(conn, data) {
|
|
|
99
99
|
const latest = history.messages[history.messages.length - 1];
|
|
100
100
|
if (!latest)
|
|
101
101
|
return;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
const ctx = conn.gatewayCtx;
|
|
103
|
+
const { channelRuntime } = ctx;
|
|
104
|
+
if (!channelRuntime) {
|
|
105
|
+
ctx.log?.warn("channelRuntime not available, skipping AI dispatch");
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// Resolve agent route for this sender
|
|
109
|
+
const route = channelRuntime.routing.resolveAgentRoute({
|
|
110
|
+
cfg: ctx.cfg,
|
|
105
111
|
channel: "openswitchy",
|
|
106
|
-
accountId:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
accountId: ctx.accountId,
|
|
113
|
+
peer: { kind: "direct", id: data.from.agentId },
|
|
114
|
+
});
|
|
115
|
+
// Build the inbound envelope body via the SDK runtime
|
|
116
|
+
const storePath = channelRuntime.session.resolveStorePath(undefined, {
|
|
117
|
+
agentId: route.agentId,
|
|
118
|
+
});
|
|
119
|
+
const envelopeOpts = channelRuntime.reply.resolveEnvelopeFormatOptions(ctx.cfg);
|
|
120
|
+
const body = channelRuntime.reply.formatAgentEnvelope({
|
|
121
|
+
channel: "openswitchy",
|
|
122
|
+
from: data.from.name,
|
|
110
123
|
timestamp: new Date(latest.createdAt).getTime() || Date.now(),
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
124
|
+
envelope: envelopeOpts,
|
|
125
|
+
body: latest.content,
|
|
126
|
+
});
|
|
127
|
+
const mentioned = data.mentioned === true ||
|
|
128
|
+
(latest.metadata?.mentionedAgentIds || []).includes(conn.agentId);
|
|
129
|
+
// Build MsgContext for the SDK pipeline
|
|
130
|
+
const msgCtx = {
|
|
131
|
+
Body: body,
|
|
132
|
+
From: data.from.agentId,
|
|
133
|
+
To: data.chatRoomId,
|
|
134
|
+
AccountId: ctx.accountId,
|
|
135
|
+
SessionKey: route.sessionKey,
|
|
136
|
+
ChatType: "direct",
|
|
137
|
+
SenderName: data.from.name,
|
|
117
138
|
};
|
|
118
|
-
|
|
139
|
+
// Record inbound session
|
|
140
|
+
const chatRoomId = data.chatRoomId;
|
|
141
|
+
await channelRuntime.session.recordInboundSession({
|
|
142
|
+
storePath,
|
|
143
|
+
sessionKey: route.sessionKey,
|
|
144
|
+
ctx: msgCtx,
|
|
145
|
+
onRecordError: (err) => {
|
|
146
|
+
ctx.log?.error(`Session record error: ${err}`);
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
// Dispatch AI reply
|
|
150
|
+
await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
151
|
+
ctx: msgCtx,
|
|
152
|
+
cfg: ctx.cfg,
|
|
153
|
+
dispatcherOptions: {
|
|
154
|
+
deliver: async (payload) => {
|
|
155
|
+
const text = payload.text;
|
|
156
|
+
if (!text)
|
|
157
|
+
return;
|
|
158
|
+
await apiCall(conn.baseUrl, conn.apiKey, "POST", "/chat", {
|
|
159
|
+
chatRoomId,
|
|
160
|
+
message: text,
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
119
165
|
}
|
|
120
166
|
function sleep(ms) {
|
|
121
167
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -137,20 +183,22 @@ export const openswitchyChannel = {
|
|
|
137
183
|
/* ── Config Adapter ── */
|
|
138
184
|
config: {
|
|
139
185
|
listAccountIds(cfg) {
|
|
140
|
-
const
|
|
141
|
-
|
|
186
|
+
const channels = cfg.channels;
|
|
187
|
+
const osSection = channels?.openswitchy;
|
|
188
|
+
if (!osSection?.accounts)
|
|
142
189
|
return [];
|
|
143
|
-
return Object.keys(accounts);
|
|
190
|
+
return Object.keys(osSection.accounts);
|
|
144
191
|
},
|
|
145
192
|
resolveAccount(cfg, accountId) {
|
|
146
|
-
const
|
|
147
|
-
|
|
193
|
+
const channels = cfg.channels;
|
|
194
|
+
const osSection = channels?.openswitchy;
|
|
195
|
+
if (!osSection?.accounts)
|
|
148
196
|
return {};
|
|
149
|
-
if (accountId && accounts[accountId])
|
|
150
|
-
return accounts[accountId];
|
|
197
|
+
if (accountId && osSection.accounts[accountId])
|
|
198
|
+
return osSection.accounts[accountId];
|
|
151
199
|
// Fall back to first account
|
|
152
|
-
const firstKey = Object.keys(accounts)[0];
|
|
153
|
-
return firstKey ? accounts[firstKey] : {};
|
|
200
|
+
const firstKey = Object.keys(osSection.accounts)[0];
|
|
201
|
+
return firstKey ? osSection.accounts[firstKey] : {};
|
|
154
202
|
},
|
|
155
203
|
isConfigured(account) {
|
|
156
204
|
return Boolean(account.joinCode && account.agentName);
|
|
@@ -167,25 +215,23 @@ export const openswitchyChannel = {
|
|
|
167
215
|
throw new Error("[openswitchy] Missing joinCode or agentName in config");
|
|
168
216
|
}
|
|
169
217
|
const baseUrl = account.url || DEFAULT_URL;
|
|
170
|
-
|
|
218
|
+
ctx.log?.info(`Registering "${account.agentName}" at ${baseUrl}`);
|
|
171
219
|
const reg = await registerAgent(account);
|
|
172
|
-
|
|
220
|
+
ctx.log?.info(`Registered as ${reg.name} (${reg.agentId}) in "${reg.orgName}" — status: ${reg.status}`);
|
|
173
221
|
const conn = {
|
|
174
222
|
apiKey: reg.apiKey,
|
|
175
223
|
agentId: reg.agentId,
|
|
176
224
|
baseUrl,
|
|
177
225
|
accountId,
|
|
178
226
|
abortController: new AbortController(),
|
|
179
|
-
|
|
180
|
-
ctx.channelRuntime?.reply.dispatchInbound(envelope);
|
|
181
|
-
},
|
|
227
|
+
gatewayCtx: ctx,
|
|
182
228
|
};
|
|
183
229
|
// Link abort to OpenClaw's signal
|
|
184
230
|
abortSignal.addEventListener("abort", () => conn.abortController.abort());
|
|
185
231
|
connections.set(accountId, conn);
|
|
186
232
|
// Start SSE listener (fire-and-forget, reconnects internally)
|
|
187
233
|
listenSse(conn);
|
|
188
|
-
|
|
234
|
+
ctx.log?.info("SSE connected, listening for messages");
|
|
189
235
|
},
|
|
190
236
|
async stopAccount(ctx) {
|
|
191
237
|
const conn = connections.get(ctx.accountId);
|
|
@@ -193,7 +239,7 @@ export const openswitchyChannel = {
|
|
|
193
239
|
conn.abortController.abort();
|
|
194
240
|
connections.delete(ctx.accountId);
|
|
195
241
|
}
|
|
196
|
-
|
|
242
|
+
ctx.log?.info(`Disconnected account ${ctx.accountId}`);
|
|
197
243
|
},
|
|
198
244
|
},
|
|
199
245
|
/* ── Outbound Adapter ── */
|
|
@@ -206,7 +252,7 @@ export const openswitchyChannel = {
|
|
|
206
252
|
throw new Error("[openswitchy] No active connection for this account");
|
|
207
253
|
}
|
|
208
254
|
const result = await apiCall(conn.baseUrl, conn.apiKey, "POST", "/chat", { chatRoomId: ctx.to, message: ctx.text });
|
|
209
|
-
return { messageId: result.messageId };
|
|
255
|
+
return { channel: "openswitchy", messageId: result.messageId };
|
|
210
256
|
},
|
|
211
257
|
},
|
|
212
258
|
/* ── Security Adapter ── */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { openswitchyChannel } from "./channel.js";
|
|
3
|
+
/* ── Helper to build a fake OpenClawConfig ── */
|
|
4
|
+
function makeConfig(accounts) {
|
|
5
|
+
return { channels: { openswitchy: { accounts } } };
|
|
6
|
+
}
|
|
7
|
+
/* ── Config Adapter ── */
|
|
8
|
+
describe("config adapter", () => {
|
|
9
|
+
const { config } = openswitchyChannel;
|
|
10
|
+
it("listAccountIds returns account IDs from config", () => {
|
|
11
|
+
const cfg = makeConfig({
|
|
12
|
+
default: { joinCode: "abc", agentName: "Bot" },
|
|
13
|
+
work: { joinCode: "xyz", agentName: "WorkBot" },
|
|
14
|
+
});
|
|
15
|
+
expect(config.listAccountIds(cfg)).toEqual(["default", "work"]);
|
|
16
|
+
});
|
|
17
|
+
it("listAccountIds returns empty array when no config", () => {
|
|
18
|
+
expect(config.listAccountIds({})).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
it("listAccountIds returns empty when no accounts section", () => {
|
|
21
|
+
const cfg = { channels: { openswitchy: {} } };
|
|
22
|
+
expect(config.listAccountIds(cfg)).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
it("resolveAccount returns the correct account config", () => {
|
|
25
|
+
const cfg = makeConfig({
|
|
26
|
+
default: { joinCode: "abc", agentName: "Bot1" },
|
|
27
|
+
work: { joinCode: "xyz", agentName: "Bot2" },
|
|
28
|
+
});
|
|
29
|
+
const account = config.resolveAccount(cfg, "work");
|
|
30
|
+
expect(account).toEqual({ joinCode: "xyz", agentName: "Bot2" });
|
|
31
|
+
});
|
|
32
|
+
it("resolveAccount falls back to first account when ID not found", () => {
|
|
33
|
+
const cfg = makeConfig({
|
|
34
|
+
default: { joinCode: "abc", agentName: "Bot1" },
|
|
35
|
+
});
|
|
36
|
+
const account = config.resolveAccount(cfg, "nonexistent");
|
|
37
|
+
expect(account).toEqual({ joinCode: "abc", agentName: "Bot1" });
|
|
38
|
+
});
|
|
39
|
+
it("resolveAccount returns empty object when no accounts", () => {
|
|
40
|
+
const account = config.resolveAccount({}, "anything");
|
|
41
|
+
expect(account).toEqual({});
|
|
42
|
+
});
|
|
43
|
+
it("isConfigured returns true when joinCode and agentName are set", () => {
|
|
44
|
+
expect(config.isConfigured({ joinCode: "abc", agentName: "Bot" }, {})).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it("isConfigured returns false when joinCode is missing", () => {
|
|
47
|
+
expect(config.isConfigured({ agentName: "Bot" }, {})).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
it("isConfigured returns false when agentName is missing", () => {
|
|
50
|
+
expect(config.isConfigured({ joinCode: "abc" }, {})).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
it("isEnabled returns true by default", () => {
|
|
53
|
+
expect(config.isEnabled({}, {})).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
it("isEnabled returns false when explicitly disabled", () => {
|
|
56
|
+
expect(config.isEnabled({ enabled: false }, {})).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it("isEnabled returns true when explicitly enabled", () => {
|
|
59
|
+
expect(config.isEnabled({ enabled: true }, {})).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
/* ── Meta & Capabilities ── */
|
|
63
|
+
describe("meta and capabilities", () => {
|
|
64
|
+
it("has correct id", () => {
|
|
65
|
+
expect(openswitchyChannel.id).toBe("openswitchy");
|
|
66
|
+
});
|
|
67
|
+
it("meta.label is OpenSwitchy", () => {
|
|
68
|
+
expect(openswitchyChannel.meta.label).toBe("OpenSwitchy");
|
|
69
|
+
});
|
|
70
|
+
it("meta has docsPath", () => {
|
|
71
|
+
expect(openswitchyChannel.meta.docsPath).toBe("channels/openswitchy");
|
|
72
|
+
});
|
|
73
|
+
it("supports direct and group chats", () => {
|
|
74
|
+
expect(openswitchyChannel.capabilities.chatTypes).toContain("direct");
|
|
75
|
+
expect(openswitchyChannel.capabilities.chatTypes).toContain("group");
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
/* ── Security Adapter ── */
|
|
79
|
+
describe("security adapter", () => {
|
|
80
|
+
const security = openswitchyChannel.security;
|
|
81
|
+
it("resolveDmPolicy returns open by default", () => {
|
|
82
|
+
const result = security.resolveDmPolicy({
|
|
83
|
+
cfg: {},
|
|
84
|
+
account: {},
|
|
85
|
+
});
|
|
86
|
+
expect(result?.policy).toBe("open");
|
|
87
|
+
});
|
|
88
|
+
it("resolveDmPolicy returns configured policy", () => {
|
|
89
|
+
const result = security.resolveDmPolicy({
|
|
90
|
+
cfg: {},
|
|
91
|
+
account: { dmPolicy: "pairing" },
|
|
92
|
+
});
|
|
93
|
+
expect(result?.policy).toBe("pairing");
|
|
94
|
+
});
|
|
95
|
+
it("resolveDmPolicy includes allowFromPath", () => {
|
|
96
|
+
const result = security.resolveDmPolicy({
|
|
97
|
+
cfg: {},
|
|
98
|
+
account: {},
|
|
99
|
+
});
|
|
100
|
+
expect(result?.allowFromPath).toBe("channels.openswitchy.allowFrom");
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
/* ── Outbound Adapter ── */
|
|
104
|
+
describe("outbound adapter", () => {
|
|
105
|
+
const outbound = openswitchyChannel.outbound;
|
|
106
|
+
it("textChunkLimit is 4096", () => {
|
|
107
|
+
expect(outbound.textChunkLimit).toBe(4096);
|
|
108
|
+
});
|
|
109
|
+
it("deliveryMode is direct", () => {
|
|
110
|
+
expect(outbound.deliveryMode).toBe("direct");
|
|
111
|
+
});
|
|
112
|
+
it("sendText throws when no active connection", async () => {
|
|
113
|
+
await expect(outbound.sendText({
|
|
114
|
+
cfg: {},
|
|
115
|
+
to: "room-123",
|
|
116
|
+
text: "hello",
|
|
117
|
+
accountId: "nonexistent",
|
|
118
|
+
})).rejects.toThrow("No active connection");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
/* ── Gateway Adapter ── */
|
|
122
|
+
describe("gateway adapter", () => {
|
|
123
|
+
const gateway = openswitchyChannel.gateway;
|
|
124
|
+
beforeEach(() => {
|
|
125
|
+
vi.restoreAllMocks();
|
|
126
|
+
});
|
|
127
|
+
it("startAccount throws when joinCode is missing", async () => {
|
|
128
|
+
const ctx = {
|
|
129
|
+
cfg: {},
|
|
130
|
+
accountId: "test",
|
|
131
|
+
account: { agentName: "Bot" },
|
|
132
|
+
abortSignal: new AbortController().signal,
|
|
133
|
+
runtime: {},
|
|
134
|
+
getStatus: vi.fn(),
|
|
135
|
+
setStatus: vi.fn(),
|
|
136
|
+
};
|
|
137
|
+
await expect(gateway.startAccount(ctx)).rejects.toThrow("Missing joinCode or agentName");
|
|
138
|
+
});
|
|
139
|
+
it("startAccount throws when agentName is missing", async () => {
|
|
140
|
+
const ctx = {
|
|
141
|
+
cfg: {},
|
|
142
|
+
accountId: "test",
|
|
143
|
+
account: { joinCode: "abc" },
|
|
144
|
+
abortSignal: new AbortController().signal,
|
|
145
|
+
runtime: {},
|
|
146
|
+
getStatus: vi.fn(),
|
|
147
|
+
setStatus: vi.fn(),
|
|
148
|
+
};
|
|
149
|
+
await expect(gateway.startAccount(ctx)).rejects.toThrow("Missing joinCode or agentName");
|
|
150
|
+
});
|
|
151
|
+
it("startAccount registers and stores connection", async () => {
|
|
152
|
+
const mockFetch = vi.fn()
|
|
153
|
+
// registration call
|
|
154
|
+
.mockResolvedValueOnce({
|
|
155
|
+
ok: true,
|
|
156
|
+
json: () => Promise.resolve({
|
|
157
|
+
agentId: "agent-1",
|
|
158
|
+
name: "TestBot",
|
|
159
|
+
orgId: "org-1",
|
|
160
|
+
orgName: "TestOrg",
|
|
161
|
+
apiKey: "sb_test_key",
|
|
162
|
+
status: "approved",
|
|
163
|
+
message: "Registered",
|
|
164
|
+
}),
|
|
165
|
+
})
|
|
166
|
+
// SSE call (return a never-resolving body to keep SSE alive)
|
|
167
|
+
.mockResolvedValueOnce({
|
|
168
|
+
ok: true,
|
|
169
|
+
body: {
|
|
170
|
+
getReader: () => ({
|
|
171
|
+
read: () => new Promise(() => { }), // hangs forever (SSE stream)
|
|
172
|
+
}),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
176
|
+
const abortController = new AbortController();
|
|
177
|
+
const ctx = {
|
|
178
|
+
cfg: {},
|
|
179
|
+
accountId: "test-acc",
|
|
180
|
+
account: { joinCode: "abc", agentName: "TestBot", url: "http://localhost:3000" },
|
|
181
|
+
abortSignal: abortController.signal,
|
|
182
|
+
runtime: {},
|
|
183
|
+
getStatus: vi.fn(),
|
|
184
|
+
setStatus: vi.fn(),
|
|
185
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
186
|
+
};
|
|
187
|
+
await gateway.startAccount(ctx);
|
|
188
|
+
// Verify registration was called
|
|
189
|
+
expect(mockFetch).toHaveBeenCalledWith("http://localhost:3000/register", expect.objectContaining({
|
|
190
|
+
method: "POST",
|
|
191
|
+
body: expect.stringContaining('"joinCode":"abc"'),
|
|
192
|
+
}));
|
|
193
|
+
// Cleanup
|
|
194
|
+
abortController.abort();
|
|
195
|
+
vi.unstubAllGlobals();
|
|
196
|
+
});
|
|
197
|
+
it("stopAccount cleans up", async () => {
|
|
198
|
+
const ctx = {
|
|
199
|
+
cfg: {},
|
|
200
|
+
accountId: "nonexistent-acc",
|
|
201
|
+
account: {},
|
|
202
|
+
abortSignal: new AbortController().signal,
|
|
203
|
+
runtime: {},
|
|
204
|
+
getStatus: vi.fn(),
|
|
205
|
+
setStatus: vi.fn(),
|
|
206
|
+
log: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
207
|
+
};
|
|
208
|
+
// Should not throw even if no connection exists
|
|
209
|
+
await expect(gateway.stopAccount(ctx)).resolves.toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
});
|
package/dist/index.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,117 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* OpenSwitchy-specific types for the channel plugin.
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw SDK types (ChannelPlugin, OpenClawPluginApi, etc.) are imported
|
|
5
|
+
* directly from "openclaw/plugin-sdk" — only OpenSwitchy API shapes live here.
|
|
4
6
|
*/
|
|
5
|
-
export interface OpenClawPluginApi {
|
|
6
|
-
id: string;
|
|
7
|
-
name: string;
|
|
8
|
-
config: OpenClawConfig;
|
|
9
|
-
logger: PluginLogger;
|
|
10
|
-
registerChannel(registration: {
|
|
11
|
-
plugin: ChannelPlugin;
|
|
12
|
-
}): void;
|
|
13
|
-
}
|
|
14
|
-
export interface PluginLogger {
|
|
15
|
-
info(msg: string, ...args: unknown[]): void;
|
|
16
|
-
warn(msg: string, ...args: unknown[]): void;
|
|
17
|
-
error(msg: string, ...args: unknown[]): void;
|
|
18
|
-
}
|
|
19
|
-
export type ChatType = "direct" | "group";
|
|
20
|
-
export interface ChannelPlugin<ResolvedAccount = unknown> {
|
|
21
|
-
id: string;
|
|
22
|
-
meta: ChannelMeta;
|
|
23
|
-
capabilities: ChannelCapabilities;
|
|
24
|
-
config: ChannelConfigAdapter<ResolvedAccount>;
|
|
25
|
-
gateway?: ChannelGatewayAdapter<ResolvedAccount>;
|
|
26
|
-
outbound?: ChannelOutboundAdapter;
|
|
27
|
-
security?: ChannelSecurityAdapter<ResolvedAccount>;
|
|
28
|
-
}
|
|
29
|
-
export interface ChannelMeta {
|
|
30
|
-
id: string;
|
|
31
|
-
label: string;
|
|
32
|
-
selectionLabel: string;
|
|
33
|
-
docsPath: string;
|
|
34
|
-
blurb: string;
|
|
35
|
-
aliases?: string[];
|
|
36
|
-
}
|
|
37
|
-
export interface ChannelCapabilities {
|
|
38
|
-
chatTypes: ChatType[];
|
|
39
|
-
media?: boolean;
|
|
40
|
-
reactions?: boolean;
|
|
41
|
-
threads?: boolean;
|
|
42
|
-
}
|
|
43
|
-
export interface ChannelConfigAdapter<ResolvedAccount> {
|
|
44
|
-
listAccountIds(cfg: OpenClawConfig): string[];
|
|
45
|
-
resolveAccount(cfg: OpenClawConfig, accountId?: string | null): ResolvedAccount;
|
|
46
|
-
isConfigured?(account: ResolvedAccount, cfg: OpenClawConfig): boolean;
|
|
47
|
-
isEnabled?(account: ResolvedAccount, cfg: OpenClawConfig): boolean;
|
|
48
|
-
}
|
|
49
|
-
export interface ChannelGatewayAdapter<ResolvedAccount> {
|
|
50
|
-
startAccount?(ctx: ChannelGatewayContext<ResolvedAccount>): Promise<unknown>;
|
|
51
|
-
stopAccount?(ctx: ChannelGatewayContext<ResolvedAccount>): Promise<void>;
|
|
52
|
-
}
|
|
53
|
-
export interface ChannelGatewayContext<ResolvedAccount> {
|
|
54
|
-
cfg: OpenClawConfig;
|
|
55
|
-
accountId: string;
|
|
56
|
-
account: ResolvedAccount;
|
|
57
|
-
abortSignal: AbortSignal;
|
|
58
|
-
log?: {
|
|
59
|
-
info(msg: string): void;
|
|
60
|
-
error(msg: string): void;
|
|
61
|
-
};
|
|
62
|
-
channelRuntime?: PluginChannelRuntime;
|
|
63
|
-
}
|
|
64
|
-
export interface PluginChannelRuntime {
|
|
65
|
-
reply: {
|
|
66
|
-
dispatchInbound(envelope: InboundEnvelope): void;
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
export interface InboundEnvelope {
|
|
70
|
-
channel: string;
|
|
71
|
-
accountId: string;
|
|
72
|
-
from: string;
|
|
73
|
-
to: string;
|
|
74
|
-
body: string;
|
|
75
|
-
timestamp?: number;
|
|
76
|
-
metadata?: Record<string, unknown>;
|
|
77
|
-
}
|
|
78
|
-
export interface ChannelOutboundAdapter {
|
|
79
|
-
deliveryMode: "direct" | "gateway" | "hybrid";
|
|
80
|
-
textChunkLimit?: number;
|
|
81
|
-
sendText?(ctx: ChannelOutboundContext): Promise<OutboundDeliveryResult>;
|
|
82
|
-
}
|
|
83
|
-
export interface ChannelOutboundContext {
|
|
84
|
-
cfg: OpenClawConfig;
|
|
85
|
-
to: string;
|
|
86
|
-
text: string;
|
|
87
|
-
accountId?: string | null;
|
|
88
|
-
}
|
|
89
|
-
export interface OutboundDeliveryResult {
|
|
90
|
-
messageId?: string;
|
|
91
|
-
}
|
|
92
|
-
export interface ChannelSecurityAdapter<ResolvedAccount> {
|
|
93
|
-
resolveDmPolicy?(ctx: ChannelSecurityContext<ResolvedAccount>): ChannelSecurityDmPolicy | null;
|
|
94
|
-
}
|
|
95
|
-
export interface ChannelSecurityContext<ResolvedAccount> {
|
|
96
|
-
cfg: OpenClawConfig;
|
|
97
|
-
accountId?: string | null;
|
|
98
|
-
account: ResolvedAccount;
|
|
99
|
-
}
|
|
100
|
-
export interface ChannelSecurityDmPolicy {
|
|
101
|
-
policy: string;
|
|
102
|
-
allowFrom?: Array<string | number> | null;
|
|
103
|
-
allowFromPath: string;
|
|
104
|
-
approveHint: string;
|
|
105
|
-
}
|
|
106
|
-
export interface OpenClawConfig {
|
|
107
|
-
channels?: {
|
|
108
|
-
openswitchy?: {
|
|
109
|
-
accounts?: Record<string, AccountConfig>;
|
|
110
|
-
};
|
|
111
|
-
[key: string]: unknown;
|
|
112
|
-
};
|
|
113
|
-
[key: string]: unknown;
|
|
114
|
-
}
|
|
115
7
|
export interface AccountConfig {
|
|
116
8
|
url?: string;
|
|
117
9
|
joinCode?: string;
|
package/dist/types.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* OpenSwitchy-specific types for the channel plugin.
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw SDK types (ChannelPlugin, OpenClawPluginApi, etc.) are imported
|
|
5
|
+
* directly from "openclaw/plugin-sdk" — only OpenSwitchy API shapes live here.
|
|
4
6
|
*/
|
|
5
7
|
export {};
|
package/package.json
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-channel-openswitchy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "OpenSwitchy channel plugin for OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
8
|
-
"files": [
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"openclaw.plugin.json"
|
|
11
|
+
],
|
|
9
12
|
"scripts": {
|
|
10
13
|
"build": "tsc",
|
|
11
14
|
"dev": "tsc --watch",
|
|
12
|
-
"prepublishOnly": "npm run build"
|
|
15
|
+
"prepublishOnly": "npm run build",
|
|
16
|
+
"test": "vitest run"
|
|
13
17
|
},
|
|
14
|
-
"keywords": [
|
|
18
|
+
"keywords": [
|
|
19
|
+
"openclaw",
|
|
20
|
+
"openswitchy",
|
|
21
|
+
"channel",
|
|
22
|
+
"plugin",
|
|
23
|
+
"ai-agents"
|
|
24
|
+
],
|
|
15
25
|
"repository": {
|
|
16
26
|
"type": "git",
|
|
17
27
|
"url": "https://github.com/OpenSwitchy/openclaw-channel-openswitchy.git"
|
|
18
28
|
},
|
|
19
29
|
"license": "MIT",
|
|
20
30
|
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.0.0",
|
|
32
|
+
"openclaw": "^2026.3.7",
|
|
21
33
|
"typescript": "^5.7.0",
|
|
22
|
-
"
|
|
34
|
+
"vitest": "^4.0.18"
|
|
23
35
|
}
|
|
24
36
|
}
|