opc-agent 1.4.0 → 2.0.1
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/CHANGELOG.md +25 -0
- package/README.md +91 -32
- package/dist/channels/email.d.ts +32 -26
- package/dist/channels/email.js +239 -62
- package/dist/channels/feishu.d.ts +21 -6
- package/dist/channels/feishu.js +225 -126
- package/dist/channels/telegram.d.ts +30 -9
- package/dist/channels/telegram.js +125 -33
- package/dist/channels/websocket.d.ts +46 -3
- package/dist/channels/websocket.js +306 -37
- package/dist/channels/wechat.d.ts +33 -13
- package/dist/channels/wechat.js +229 -42
- package/dist/cli.js +1127 -19
- package/dist/core/a2a.d.ts +17 -0
- package/dist/core/a2a.js +43 -1
- package/dist/core/agent.d.ts +39 -0
- package/dist/core/agent.js +228 -3
- package/dist/core/runtime.d.ts +7 -0
- package/dist/core/runtime.js +205 -2
- package/dist/core/sandbox.d.ts +26 -0
- package/dist/core/sandbox.js +117 -0
- package/dist/core/scheduler.d.ts +52 -0
- package/dist/core/scheduler.js +168 -0
- package/dist/core/subagent.d.ts +28 -0
- package/dist/core/subagent.js +65 -0
- package/dist/core/workflow-graph.d.ts +93 -0
- package/dist/core/workflow-graph.js +247 -0
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.js +134 -0
- package/dist/doctor.d.ts +15 -0
- package/dist/doctor.js +183 -0
- package/dist/eval/index.d.ts +65 -0
- package/dist/eval/index.js +191 -0
- package/dist/index.d.ts +37 -6
- package/dist/index.js +75 -3
- package/dist/plugins/content-filter.d.ts +7 -0
- package/dist/plugins/content-filter.js +25 -0
- package/dist/plugins/index.d.ts +42 -0
- package/dist/plugins/index.js +108 -2
- package/dist/plugins/logger.d.ts +6 -0
- package/dist/plugins/logger.js +20 -0
- package/dist/plugins/rate-limiter.d.ts +7 -0
- package/dist/plugins/rate-limiter.js +35 -0
- package/dist/protocols/a2a/client.d.ts +25 -0
- package/dist/protocols/a2a/client.js +115 -0
- package/dist/protocols/a2a/index.d.ts +6 -0
- package/dist/protocols/a2a/index.js +12 -0
- package/dist/protocols/a2a/server.d.ts +41 -0
- package/dist/protocols/a2a/server.js +295 -0
- package/dist/protocols/a2a/types.d.ts +91 -0
- package/dist/protocols/a2a/types.js +15 -0
- package/dist/protocols/a2a/utils.d.ts +6 -0
- package/dist/protocols/a2a/utils.js +47 -0
- package/dist/protocols/agui/client.d.ts +10 -0
- package/dist/protocols/agui/client.js +75 -0
- package/dist/protocols/agui/index.d.ts +4 -0
- package/dist/protocols/agui/index.js +25 -0
- package/dist/protocols/agui/server.d.ts +37 -0
- package/dist/protocols/agui/server.js +191 -0
- package/dist/protocols/agui/types.d.ts +107 -0
- package/dist/protocols/agui/types.js +17 -0
- package/dist/protocols/index.d.ts +2 -0
- package/dist/protocols/index.js +19 -0
- package/dist/protocols/mcp/agent-tools.d.ts +11 -0
- package/dist/protocols/mcp/agent-tools.js +129 -0
- package/dist/protocols/mcp/index.d.ts +5 -0
- package/dist/protocols/mcp/index.js +11 -0
- package/dist/protocols/mcp/server.d.ts +31 -0
- package/dist/protocols/mcp/server.js +248 -0
- package/dist/protocols/mcp/types.d.ts +92 -0
- package/dist/protocols/mcp/types.js +17 -0
- package/dist/providers/index.d.ts +5 -1
- package/dist/providers/index.js +16 -9
- package/dist/publish/index.d.ts +45 -0
- package/dist/publish/index.js +350 -0
- package/dist/schema/oad.d.ts +859 -67
- package/dist/schema/oad.js +47 -3
- package/dist/security/approval.d.ts +36 -0
- package/dist/security/approval.js +113 -0
- package/dist/security/index.d.ts +4 -0
- package/dist/security/index.js +8 -0
- package/dist/security/keys.d.ts +16 -0
- package/dist/security/keys.js +117 -0
- package/dist/skills/auto-learn.d.ts +28 -0
- package/dist/skills/auto-learn.js +257 -0
- package/dist/studio/server.d.ts +63 -0
- package/dist/studio/server.js +625 -0
- package/dist/studio-ui/index.html +662 -0
- package/dist/telemetry/index.d.ts +93 -0
- package/dist/telemetry/index.js +285 -0
- package/dist/tools/builtin/datetime.d.ts +3 -0
- package/dist/tools/builtin/datetime.js +44 -0
- package/dist/tools/builtin/file.d.ts +3 -0
- package/dist/tools/builtin/file.js +151 -0
- package/dist/tools/builtin/index.d.ts +15 -0
- package/dist/tools/builtin/index.js +30 -0
- package/dist/tools/builtin/shell.d.ts +3 -0
- package/dist/tools/builtin/shell.js +43 -0
- package/dist/tools/builtin/web.d.ts +3 -0
- package/dist/tools/builtin/web.js +37 -0
- package/dist/tools/mcp-client.d.ts +24 -0
- package/dist/tools/mcp-client.js +119 -0
- package/package.json +5 -3
- package/scripts/install.ps1 +31 -0
- package/scripts/install.sh +40 -0
- package/src/channels/email.ts +351 -177
- package/src/channels/feishu.ts +349 -236
- package/src/channels/telegram.ts +212 -90
- package/src/channels/websocket.ts +399 -87
- package/src/channels/wechat.ts +329 -149
- package/src/cli.ts +1201 -20
- package/src/core/a2a.ts +60 -0
- package/src/core/agent.ts +420 -152
- package/src/core/runtime.ts +174 -0
- package/src/core/sandbox.ts +143 -0
- package/src/core/scheduler.ts +187 -0
- package/src/core/subagent.ts +98 -0
- package/src/core/workflow-graph.ts +365 -0
- package/src/daemon.ts +96 -0
- package/src/doctor.ts +156 -0
- package/src/eval/index.ts +211 -0
- package/src/eval/suites/basic.json +16 -0
- package/src/eval/suites/memory.json +12 -0
- package/src/eval/suites/safety.json +14 -0
- package/src/index.ts +65 -6
- package/src/plugins/content-filter.ts +23 -0
- package/src/plugins/index.ts +133 -2
- package/src/plugins/logger.ts +18 -0
- package/src/plugins/rate-limiter.ts +38 -0
- package/src/protocols/a2a/client.ts +132 -0
- package/src/protocols/a2a/index.ts +8 -0
- package/src/protocols/a2a/server.ts +333 -0
- package/src/protocols/a2a/types.ts +88 -0
- package/src/protocols/a2a/utils.ts +50 -0
- package/src/protocols/agui/client.ts +83 -0
- package/src/protocols/agui/index.ts +4 -0
- package/src/protocols/agui/server.ts +218 -0
- package/src/protocols/agui/types.ts +153 -0
- package/src/protocols/index.ts +2 -0
- package/src/protocols/mcp/agent-tools.ts +134 -0
- package/src/protocols/mcp/index.ts +8 -0
- package/src/protocols/mcp/server.ts +262 -0
- package/src/protocols/mcp/types.ts +69 -0
- package/src/providers/index.ts +354 -339
- package/src/publish/index.ts +376 -0
- package/src/schema/oad.ts +204 -154
- package/src/security/approval.ts +131 -0
- package/src/security/index.ts +3 -0
- package/src/security/keys.ts +87 -0
- package/src/skills/auto-learn.ts +262 -0
- package/src/studio/server.ts +629 -0
- package/src/studio-ui/index.html +662 -0
- package/src/telemetry/index.ts +324 -0
- package/src/tools/builtin/datetime.ts +41 -0
- package/src/tools/builtin/file.ts +107 -0
- package/src/tools/builtin/index.ts +28 -0
- package/src/tools/builtin/shell.ts +43 -0
- package/src/tools/builtin/web.ts +35 -0
- package/src/tools/mcp-client.ts +131 -0
- package/src/types/agent-workstation.d.ts +2 -0
- package/tests/a2a-protocol.test.ts +285 -0
- package/tests/agui-protocol.test.ts +246 -0
- package/tests/auto-learn.test.ts +105 -0
- package/tests/builtin-tools.test.ts +83 -0
- package/tests/channels/discord.test.ts +79 -0
- package/tests/channels/email.test.ts +148 -0
- package/tests/channels/feishu.test.ts +123 -0
- package/tests/channels/telegram.test.ts +129 -0
- package/tests/channels/websocket.test.ts +53 -0
- package/tests/channels/wechat.test.ts +170 -0
- package/tests/chat-cli.test.ts +160 -0
- package/tests/cli.test.ts +46 -0
- package/tests/daemon.test.ts +135 -0
- package/tests/deepbrain-wire.test.ts +234 -0
- package/tests/doctor.test.ts +38 -0
- package/tests/eval.test.ts +173 -0
- package/tests/init-role.test.ts +124 -0
- package/tests/mcp-client.test.ts +92 -0
- package/tests/mcp-server.test.ts +178 -0
- package/tests/plugin-a2a-enhanced.test.ts +230 -0
- package/tests/publish.test.ts +231 -0
- package/tests/scheduler.test.ts +200 -0
- package/tests/security-enhanced.test.ts +233 -0
- package/tests/skill-learner.test.ts +161 -0
- package/tests/studio.test.ts +229 -0
- package/tests/subagent.test.ts +193 -0
- package/tests/telegram-discord.test.ts +60 -0
- package/tests/telemetry.test.ts +186 -0
- package/tests/tools/builtin-extended.test.ts +138 -0
- package/tests/workflow-graph.test.ts +279 -0
- package/tutorial/customer-service-agent/README.md +612 -0
- package/tutorial/customer-service-agent/SOUL.md +26 -0
- package/tutorial/customer-service-agent/agent.yaml +63 -0
- package/tutorial/customer-service-agent/package.json +19 -0
- package/tutorial/customer-service-agent/src/index.ts +69 -0
- package/tutorial/customer-service-agent/src/skills/faq.ts +27 -0
- package/tutorial/customer-service-agent/src/skills/ticket.ts +22 -0
- package/tutorial/customer-service-agent/tsconfig.json +14 -0
|
@@ -35,58 +35,105 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.TelegramChannel = void 0;
|
|
37
37
|
const index_1 = require("./index");
|
|
38
|
-
/**
|
|
39
|
-
* Telegram channel — basic webhook handler for Telegram Bot API.
|
|
40
|
-
* Set TELEGRAM_BOT_TOKEN env var or pass in config.
|
|
41
|
-
*/
|
|
42
38
|
class TelegramChannel extends index_1.BaseChannel {
|
|
43
39
|
type = 'telegram';
|
|
44
40
|
token;
|
|
41
|
+
mode;
|
|
45
42
|
webhookUrl;
|
|
46
|
-
server = null;
|
|
47
43
|
port;
|
|
48
|
-
|
|
44
|
+
// Polling state
|
|
45
|
+
offset = 0;
|
|
46
|
+
polling = false;
|
|
47
|
+
// Webhook state
|
|
48
|
+
server = null;
|
|
49
|
+
constructor(config = {}) {
|
|
49
50
|
super();
|
|
50
|
-
this.token =
|
|
51
|
-
this.
|
|
52
|
-
this.
|
|
51
|
+
this.token = config.token ?? process.env.TELEGRAM_BOT_TOKEN ?? '';
|
|
52
|
+
this.mode = config.mode ?? 'polling';
|
|
53
|
+
this.webhookUrl = config.webhookUrl;
|
|
54
|
+
this.port = config.port ?? 3001;
|
|
53
55
|
}
|
|
54
56
|
async start() {
|
|
55
57
|
if (!this.token) {
|
|
56
58
|
console.warn('[TelegramChannel] No bot token provided. Set TELEGRAM_BOT_TOKEN or pass token in config.');
|
|
57
59
|
return;
|
|
58
60
|
}
|
|
61
|
+
if (this.mode === 'webhook') {
|
|
62
|
+
await this.startWebhook();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
await this.startPolling();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async stop() {
|
|
69
|
+
if (this.mode === 'webhook') {
|
|
70
|
+
await this.stopWebhook();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
this.polling = false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ─── Polling Mode ────────────────────────────────────────
|
|
77
|
+
async startPolling() {
|
|
78
|
+
// Delete any existing webhook so polling works
|
|
79
|
+
await this.apiCall('deleteWebhook');
|
|
80
|
+
console.log(`[TelegramChannel] Started long-polling mode`);
|
|
81
|
+
this.polling = true;
|
|
82
|
+
this.poll();
|
|
83
|
+
}
|
|
84
|
+
async poll() {
|
|
85
|
+
while (this.polling) {
|
|
86
|
+
try {
|
|
87
|
+
const updates = await this.getUpdates();
|
|
88
|
+
for (const update of updates) {
|
|
89
|
+
await this.processUpdate(update);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
console.error('[TelegramChannel] Polling error:', err);
|
|
94
|
+
// Back off on error
|
|
95
|
+
if (this.polling) {
|
|
96
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async getUpdates() {
|
|
102
|
+
const url = `https://api.telegram.org/bot${this.token}/getUpdates?offset=${this.offset}&timeout=30&allowed_updates=["message"]`;
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
const timeout = setTimeout(() => controller.abort(), 40000); // 30s long-poll + 10s buffer
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
107
|
+
const data = (await res.json());
|
|
108
|
+
if (data.ok && data.result.length > 0) {
|
|
109
|
+
this.offset = data.result[data.result.length - 1].update_id + 1;
|
|
110
|
+
}
|
|
111
|
+
return data.result || [];
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
clearTimeout(timeout);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// ─── Webhook Mode ────────────────────────────────────────
|
|
118
|
+
async startWebhook() {
|
|
119
|
+
if (this.webhookUrl) {
|
|
120
|
+
await this.apiCall('setWebhook', { url: `${this.webhookUrl}/webhook/${this.token}` });
|
|
121
|
+
}
|
|
59
122
|
const express = (await Promise.resolve().then(() => __importStar(require('express')))).default;
|
|
60
123
|
const app = express();
|
|
61
124
|
app.use(express.json());
|
|
62
125
|
app.post(`/webhook/${this.token}`, async (req, res) => {
|
|
63
126
|
try {
|
|
64
|
-
|
|
65
|
-
if (update.message?.text && this.handler) {
|
|
66
|
-
const msg = {
|
|
67
|
-
id: `tg_${update.message.message_id}`,
|
|
68
|
-
role: 'user',
|
|
69
|
-
content: update.message.text,
|
|
70
|
-
timestamp: update.message.date * 1000,
|
|
71
|
-
metadata: {
|
|
72
|
-
sessionId: `tg_${update.message.chat.id}`,
|
|
73
|
-
chatId: update.message.chat.id,
|
|
74
|
-
userId: update.message.from?.id,
|
|
75
|
-
platform: 'telegram',
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
const response = await this.handler(msg);
|
|
79
|
-
await this.sendMessage(update.message.chat.id, response.content);
|
|
80
|
-
}
|
|
127
|
+
await this.processUpdate(req.body);
|
|
81
128
|
res.json({ ok: true });
|
|
82
129
|
}
|
|
83
130
|
catch (err) {
|
|
84
|
-
console.error('[TelegramChannel]
|
|
131
|
+
console.error('[TelegramChannel] Webhook error:', err);
|
|
85
132
|
res.status(500).json({ error: 'Internal error' });
|
|
86
133
|
}
|
|
87
134
|
});
|
|
88
135
|
app.get('/health', (_req, res) => {
|
|
89
|
-
res.json({ status: 'ok', channel: 'telegram' });
|
|
136
|
+
res.json({ status: 'ok', channel: 'telegram', mode: 'webhook' });
|
|
90
137
|
});
|
|
91
138
|
return new Promise((resolve) => {
|
|
92
139
|
this.server = app.listen(this.port, () => {
|
|
@@ -95,25 +142,70 @@ class TelegramChannel extends index_1.BaseChannel {
|
|
|
95
142
|
});
|
|
96
143
|
});
|
|
97
144
|
}
|
|
98
|
-
async
|
|
145
|
+
async stopWebhook() {
|
|
99
146
|
return new Promise((resolve, reject) => {
|
|
100
147
|
if (!this.server)
|
|
101
148
|
return resolve();
|
|
102
149
|
this.server.close((err) => (err ? reject(err) : resolve()));
|
|
103
150
|
});
|
|
104
151
|
}
|
|
152
|
+
// ─── Shared ──────────────────────────────────────────────
|
|
153
|
+
async processUpdate(update) {
|
|
154
|
+
const message = update.message || update.edited_message;
|
|
155
|
+
if (!message?.text || !this.handler)
|
|
156
|
+
return;
|
|
157
|
+
const msg = {
|
|
158
|
+
id: `tg_${message.message_id}`,
|
|
159
|
+
role: 'user',
|
|
160
|
+
content: message.text,
|
|
161
|
+
timestamp: message.date * 1000,
|
|
162
|
+
metadata: {
|
|
163
|
+
sessionId: `tg_${message.chat.id}`,
|
|
164
|
+
chatId: message.chat.id,
|
|
165
|
+
userId: message.from?.id,
|
|
166
|
+
username: message.from?.username,
|
|
167
|
+
firstName: message.from?.first_name,
|
|
168
|
+
platform: 'telegram',
|
|
169
|
+
chatType: message.chat.type,
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
const response = await this.handler(msg);
|
|
173
|
+
await this.sendMessage(message.chat.id, response.content);
|
|
174
|
+
}
|
|
105
175
|
async sendMessage(chatId, text) {
|
|
106
|
-
|
|
176
|
+
// Telegram max message length is 4096
|
|
177
|
+
const chunks = this.splitText(text, 4096);
|
|
178
|
+
for (const chunk of chunks) {
|
|
179
|
+
await this.apiCall('sendMessage', {
|
|
180
|
+
chat_id: chatId,
|
|
181
|
+
text: chunk,
|
|
182
|
+
parse_mode: 'Markdown',
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async apiCall(method, body) {
|
|
187
|
+
const url = `https://api.telegram.org/bot${this.token}/${method}`;
|
|
107
188
|
try {
|
|
108
|
-
await fetch(url, {
|
|
189
|
+
const res = await fetch(url, {
|
|
109
190
|
method: 'POST',
|
|
110
191
|
headers: { 'Content-Type': 'application/json' },
|
|
111
|
-
body: JSON.stringify(
|
|
192
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
112
193
|
});
|
|
194
|
+
return await res.json();
|
|
113
195
|
}
|
|
114
196
|
catch (err) {
|
|
115
|
-
console.error(
|
|
197
|
+
console.error(`[TelegramChannel] API call ${method} failed:`, err);
|
|
198
|
+
throw err;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
splitText(text, maxLen) {
|
|
202
|
+
if (text.length <= maxLen)
|
|
203
|
+
return [text];
|
|
204
|
+
const parts = [];
|
|
205
|
+
for (let i = 0; i < text.length; i += maxLen) {
|
|
206
|
+
parts.push(text.slice(i, i + maxLen));
|
|
116
207
|
}
|
|
208
|
+
return parts;
|
|
117
209
|
}
|
|
118
210
|
}
|
|
119
211
|
exports.TelegramChannel = TelegramChannel;
|
|
@@ -1,15 +1,58 @@
|
|
|
1
1
|
import { BaseChannel } from './index';
|
|
2
2
|
/**
|
|
3
|
-
* WebSocket
|
|
3
|
+
* WebSocket Channel — v1.1.0
|
|
4
|
+
*
|
|
5
|
+
* Enhanced with:
|
|
6
|
+
* - Room support (multiple clients in a room)
|
|
7
|
+
* - Heartbeat/ping-pong to detect disconnected clients
|
|
8
|
+
* - Reconnection handling (session persistence)
|
|
9
|
+
* - Binary message support
|
|
10
|
+
* - Connection authentication (optional token in query string)
|
|
4
11
|
*/
|
|
12
|
+
export interface WebSocketChannelConfig {
|
|
13
|
+
port?: number;
|
|
14
|
+
/** Heartbeat interval in ms (default: 30000) */
|
|
15
|
+
heartbeatInterval?: number;
|
|
16
|
+
/** Valid auth tokens (if empty, no auth required) */
|
|
17
|
+
authTokens?: string[];
|
|
18
|
+
/** Max clients per room (default: 100) */
|
|
19
|
+
maxClientsPerRoom?: number;
|
|
20
|
+
}
|
|
5
21
|
export declare class WebSocketChannel extends BaseChannel {
|
|
6
22
|
readonly type = "websocket";
|
|
7
23
|
private wss;
|
|
8
|
-
private
|
|
24
|
+
private config;
|
|
9
25
|
private clients;
|
|
10
|
-
|
|
26
|
+
private rooms;
|
|
27
|
+
private heartbeatTimer;
|
|
28
|
+
constructor(configOrPort?: number | WebSocketChannelConfig);
|
|
11
29
|
start(): Promise<void>;
|
|
12
30
|
stop(): Promise<void>;
|
|
31
|
+
/** Handle text (JSON) messages */
|
|
32
|
+
private handleTextMessage;
|
|
33
|
+
/** Handle binary messages */
|
|
34
|
+
private handleBinaryMessage;
|
|
35
|
+
/** Join a room */
|
|
36
|
+
joinRoom(sessionId: string, roomId: string): boolean;
|
|
37
|
+
/** Leave a room */
|
|
38
|
+
leaveRoom(sessionId: string, roomId: string): void;
|
|
39
|
+
/** Remove client completely */
|
|
40
|
+
private removeClient;
|
|
41
|
+
/** Get room member session IDs */
|
|
42
|
+
getRoomMembers(roomId: string): string[];
|
|
43
|
+
/** Get all rooms */
|
|
44
|
+
getRooms(): string[];
|
|
45
|
+
/** Broadcast to all clients */
|
|
13
46
|
broadcast(content: string): void;
|
|
47
|
+
/** Broadcast to all clients in a room */
|
|
48
|
+
broadcastToRoom(roomId: string, data: any, excludeSessionId?: string): void;
|
|
49
|
+
/** Send to specific session */
|
|
50
|
+
sendToSession(sessionId: string, data: any): boolean;
|
|
51
|
+
/** Get connection stats */
|
|
52
|
+
getStats(): {
|
|
53
|
+
clients: number;
|
|
54
|
+
rooms: number;
|
|
55
|
+
roomDetails: Record<string, number>;
|
|
56
|
+
};
|
|
14
57
|
}
|
|
15
58
|
//# sourceMappingURL=websocket.d.ts.map
|
|
@@ -3,79 +3,348 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WebSocketChannel = void 0;
|
|
4
4
|
const index_1 = require("./index");
|
|
5
5
|
const ws_1 = require("ws");
|
|
6
|
-
/**
|
|
7
|
-
* WebSocket channel — real-time bidirectional communication.
|
|
8
|
-
*/
|
|
9
6
|
class WebSocketChannel extends index_1.BaseChannel {
|
|
10
7
|
type = 'websocket';
|
|
11
8
|
wss = null;
|
|
12
|
-
|
|
13
|
-
clients = new
|
|
14
|
-
|
|
9
|
+
config;
|
|
10
|
+
clients = new Map(); // sessionId -> ClientInfo
|
|
11
|
+
rooms = new Map(); // roomId -> Set<sessionId>
|
|
12
|
+
heartbeatTimer = null;
|
|
13
|
+
constructor(configOrPort = 3002) {
|
|
15
14
|
super();
|
|
16
|
-
|
|
15
|
+
if (typeof configOrPort === 'number') {
|
|
16
|
+
this.config = { port: configOrPort, heartbeatInterval: 30000, authTokens: [], maxClientsPerRoom: 100 };
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
this.config = {
|
|
20
|
+
port: configOrPort.port ?? 3002,
|
|
21
|
+
heartbeatInterval: configOrPort.heartbeatInterval ?? 30000,
|
|
22
|
+
authTokens: configOrPort.authTokens ?? [],
|
|
23
|
+
maxClientsPerRoom: configOrPort.maxClientsPerRoom ?? 100,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
17
26
|
}
|
|
18
27
|
async start() {
|
|
19
28
|
return new Promise((resolve) => {
|
|
20
|
-
this.wss = new ws_1.WebSocketServer({ port: this.port });
|
|
21
|
-
this.wss.on('connection', (ws) => {
|
|
22
|
-
this.
|
|
23
|
-
|
|
24
|
-
|
|
29
|
+
this.wss = new ws_1.WebSocketServer({ port: this.config.port });
|
|
30
|
+
this.wss.on('connection', (ws, req) => {
|
|
31
|
+
const url = new URL(req.url ?? '/', `http://localhost:${this.config.port}`);
|
|
32
|
+
const token = url.searchParams.get('token');
|
|
33
|
+
const sessionId = url.searchParams.get('sessionId') ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
34
|
+
// Authentication check
|
|
35
|
+
if (this.config.authTokens.length > 0) {
|
|
36
|
+
if (!token || !this.config.authTokens.includes(token)) {
|
|
37
|
+
ws.close(4001, 'Unauthorized');
|
|
25
38
|
return;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Handle reconnection: if sessionId already exists, replace the connection
|
|
42
|
+
const existing = this.clients.get(sessionId);
|
|
43
|
+
if (existing) {
|
|
44
|
+
try {
|
|
45
|
+
existing.ws.close(4000, 'Replaced by new connection');
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
}
|
|
49
|
+
const clientInfo = {
|
|
50
|
+
ws,
|
|
51
|
+
sessionId,
|
|
52
|
+
rooms: existing?.rooms ?? new Set(),
|
|
53
|
+
isAlive: true,
|
|
54
|
+
authenticated: true,
|
|
55
|
+
connectedAt: Date.now(),
|
|
56
|
+
lastMessageAt: Date.now(),
|
|
57
|
+
};
|
|
58
|
+
this.clients.set(sessionId, clientInfo);
|
|
59
|
+
// Re-register in rooms after reconnect
|
|
60
|
+
for (const roomId of clientInfo.rooms) {
|
|
61
|
+
const room = this.rooms.get(roomId);
|
|
62
|
+
if (room)
|
|
63
|
+
room.add(sessionId);
|
|
64
|
+
}
|
|
65
|
+
ws.on('pong', () => {
|
|
66
|
+
clientInfo.isAlive = true;
|
|
67
|
+
});
|
|
68
|
+
ws.on('message', async (data, isBinary) => {
|
|
69
|
+
clientInfo.lastMessageAt = Date.now();
|
|
70
|
+
clientInfo.isAlive = true;
|
|
71
|
+
if (isBinary) {
|
|
72
|
+
await this.handleBinaryMessage(clientInfo, data);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
26
75
|
try {
|
|
27
76
|
const parsed = JSON.parse(data.toString());
|
|
28
|
-
|
|
29
|
-
id: parsed.id ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
30
|
-
role: 'user',
|
|
31
|
-
content: parsed.content ?? parsed.message ?? data.toString(),
|
|
32
|
-
timestamp: Date.now(),
|
|
33
|
-
metadata: {
|
|
34
|
-
sessionId: parsed.sessionId ?? 'ws-default',
|
|
35
|
-
platform: 'websocket',
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
const response = await this.handler(msg);
|
|
39
|
-
ws.send(JSON.stringify({
|
|
40
|
-
id: response.id,
|
|
41
|
-
content: response.content,
|
|
42
|
-
timestamp: response.timestamp,
|
|
43
|
-
}));
|
|
77
|
+
await this.handleTextMessage(clientInfo, parsed);
|
|
44
78
|
}
|
|
45
|
-
catch
|
|
79
|
+
catch {
|
|
46
80
|
ws.send(JSON.stringify({ error: 'Invalid message format' }));
|
|
47
81
|
}
|
|
48
82
|
});
|
|
49
83
|
ws.on('close', () => {
|
|
50
|
-
|
|
84
|
+
// Don't immediately remove - allow reconnection window
|
|
85
|
+
const info = this.clients.get(sessionId);
|
|
86
|
+
if (info && info.ws === ws) {
|
|
87
|
+
// Mark as disconnected but keep for potential reconnection
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
const current = this.clients.get(sessionId);
|
|
90
|
+
if (current && current.ws === ws) {
|
|
91
|
+
this.removeClient(sessionId);
|
|
92
|
+
}
|
|
93
|
+
}, 60000); // 60s reconnection window
|
|
94
|
+
}
|
|
51
95
|
});
|
|
52
|
-
ws.send(JSON.stringify({
|
|
96
|
+
ws.send(JSON.stringify({
|
|
97
|
+
type: 'connected',
|
|
98
|
+
sessionId,
|
|
99
|
+
timestamp: Date.now(),
|
|
100
|
+
}));
|
|
53
101
|
});
|
|
102
|
+
// Start heartbeat
|
|
103
|
+
this.heartbeatTimer = setInterval(() => {
|
|
104
|
+
for (const [sessionId, info] of this.clients) {
|
|
105
|
+
if (!info.isAlive) {
|
|
106
|
+
try {
|
|
107
|
+
info.ws.terminate();
|
|
108
|
+
}
|
|
109
|
+
catch { }
|
|
110
|
+
this.removeClient(sessionId);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
info.isAlive = false;
|
|
114
|
+
try {
|
|
115
|
+
info.ws.ping();
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
}
|
|
119
|
+
}, this.config.heartbeatInterval);
|
|
54
120
|
this.wss.on('listening', () => {
|
|
55
|
-
console.log(`[WebSocketChannel] Listening on port ${this.port}`);
|
|
121
|
+
console.log(`[WebSocketChannel] Listening on port ${this.config.port}`);
|
|
56
122
|
resolve();
|
|
57
123
|
});
|
|
58
124
|
});
|
|
59
125
|
}
|
|
60
126
|
async stop() {
|
|
61
|
-
|
|
62
|
-
|
|
127
|
+
if (this.heartbeatTimer) {
|
|
128
|
+
clearInterval(this.heartbeatTimer);
|
|
129
|
+
this.heartbeatTimer = null;
|
|
130
|
+
}
|
|
131
|
+
for (const [, info] of this.clients) {
|
|
132
|
+
try {
|
|
133
|
+
info.ws.close();
|
|
134
|
+
}
|
|
135
|
+
catch { }
|
|
63
136
|
}
|
|
64
137
|
this.clients.clear();
|
|
138
|
+
this.rooms.clear();
|
|
65
139
|
return new Promise((resolve, reject) => {
|
|
66
140
|
if (!this.wss)
|
|
67
141
|
return resolve();
|
|
68
142
|
this.wss.close((err) => (err ? reject(err) : resolve()));
|
|
69
143
|
});
|
|
70
144
|
}
|
|
145
|
+
/** Handle text (JSON) messages */
|
|
146
|
+
async handleTextMessage(client, parsed) {
|
|
147
|
+
const type = parsed.type ?? 'message';
|
|
148
|
+
switch (type) {
|
|
149
|
+
case 'join': {
|
|
150
|
+
const roomId = parsed.room;
|
|
151
|
+
if (!roomId) {
|
|
152
|
+
client.ws.send(JSON.stringify({ error: 'room is required for join' }));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.joinRoom(client.sessionId, roomId);
|
|
156
|
+
client.ws.send(JSON.stringify({ type: 'joined', room: roomId, members: this.getRoomMembers(roomId).length }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
case 'leave': {
|
|
160
|
+
const roomId = parsed.room;
|
|
161
|
+
if (roomId) {
|
|
162
|
+
this.leaveRoom(client.sessionId, roomId);
|
|
163
|
+
client.ws.send(JSON.stringify({ type: 'left', room: roomId }));
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
case 'room_message': {
|
|
168
|
+
const roomId = parsed.room;
|
|
169
|
+
const content = parsed.content ?? parsed.message;
|
|
170
|
+
if (roomId && content) {
|
|
171
|
+
this.broadcastToRoom(roomId, {
|
|
172
|
+
type: 'room_message',
|
|
173
|
+
room: roomId,
|
|
174
|
+
from: client.sessionId,
|
|
175
|
+
content,
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
}, client.sessionId);
|
|
178
|
+
}
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
case 'ping': {
|
|
182
|
+
client.ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
default: {
|
|
186
|
+
// Regular chat message
|
|
187
|
+
if (!this.handler)
|
|
188
|
+
return;
|
|
189
|
+
const msg = {
|
|
190
|
+
id: parsed.id ?? `ws_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
191
|
+
role: 'user',
|
|
192
|
+
content: parsed.content ?? parsed.message ?? JSON.stringify(parsed),
|
|
193
|
+
timestamp: Date.now(),
|
|
194
|
+
metadata: {
|
|
195
|
+
sessionId: client.sessionId,
|
|
196
|
+
platform: 'websocket',
|
|
197
|
+
room: parsed.room,
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
const response = await this.handler(msg);
|
|
201
|
+
client.ws.send(JSON.stringify({
|
|
202
|
+
id: response.id,
|
|
203
|
+
content: response.content,
|
|
204
|
+
timestamp: response.timestamp,
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Handle binary messages */
|
|
210
|
+
async handleBinaryMessage(client, data) {
|
|
211
|
+
// Emit binary data with metadata
|
|
212
|
+
client.ws.send(JSON.stringify({
|
|
213
|
+
type: 'binary_ack',
|
|
214
|
+
size: data.length,
|
|
215
|
+
timestamp: Date.now(),
|
|
216
|
+
}));
|
|
217
|
+
// If handler exists, pass as base64
|
|
218
|
+
if (this.handler) {
|
|
219
|
+
const msg = {
|
|
220
|
+
id: `ws_bin_${Date.now()}`,
|
|
221
|
+
role: 'user',
|
|
222
|
+
content: `[binary:${data.length} bytes]`,
|
|
223
|
+
timestamp: Date.now(),
|
|
224
|
+
metadata: {
|
|
225
|
+
sessionId: client.sessionId,
|
|
226
|
+
platform: 'websocket',
|
|
227
|
+
binary: true,
|
|
228
|
+
binaryData: data.toString('base64'),
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
const response = await this.handler(msg);
|
|
232
|
+
client.ws.send(JSON.stringify({
|
|
233
|
+
id: response.id,
|
|
234
|
+
content: response.content,
|
|
235
|
+
timestamp: response.timestamp,
|
|
236
|
+
}));
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** Join a room */
|
|
240
|
+
joinRoom(sessionId, roomId) {
|
|
241
|
+
const client = this.clients.get(sessionId);
|
|
242
|
+
if (!client)
|
|
243
|
+
return false;
|
|
244
|
+
if (!this.rooms.has(roomId)) {
|
|
245
|
+
this.rooms.set(roomId, new Set());
|
|
246
|
+
}
|
|
247
|
+
const room = this.rooms.get(roomId);
|
|
248
|
+
if (room.size >= this.config.maxClientsPerRoom) {
|
|
249
|
+
client.ws.send(JSON.stringify({ error: 'Room is full', room: roomId }));
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
room.add(sessionId);
|
|
253
|
+
client.rooms.add(roomId);
|
|
254
|
+
// Notify other room members
|
|
255
|
+
this.broadcastToRoom(roomId, {
|
|
256
|
+
type: 'member_joined',
|
|
257
|
+
room: roomId,
|
|
258
|
+
sessionId,
|
|
259
|
+
members: room.size,
|
|
260
|
+
timestamp: Date.now(),
|
|
261
|
+
}, sessionId);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
/** Leave a room */
|
|
265
|
+
leaveRoom(sessionId, roomId) {
|
|
266
|
+
const client = this.clients.get(sessionId);
|
|
267
|
+
if (client)
|
|
268
|
+
client.rooms.delete(roomId);
|
|
269
|
+
const room = this.rooms.get(roomId);
|
|
270
|
+
if (room) {
|
|
271
|
+
room.delete(sessionId);
|
|
272
|
+
if (room.size === 0) {
|
|
273
|
+
this.rooms.delete(roomId);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
this.broadcastToRoom(roomId, {
|
|
277
|
+
type: 'member_left',
|
|
278
|
+
room: roomId,
|
|
279
|
+
sessionId,
|
|
280
|
+
members: room.size,
|
|
281
|
+
timestamp: Date.now(),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/** Remove client completely */
|
|
287
|
+
removeClient(sessionId) {
|
|
288
|
+
const client = this.clients.get(sessionId);
|
|
289
|
+
if (!client)
|
|
290
|
+
return;
|
|
291
|
+
for (const roomId of client.rooms) {
|
|
292
|
+
this.leaveRoom(sessionId, roomId);
|
|
293
|
+
}
|
|
294
|
+
this.clients.delete(sessionId);
|
|
295
|
+
}
|
|
296
|
+
/** Get room member session IDs */
|
|
297
|
+
getRoomMembers(roomId) {
|
|
298
|
+
return [...(this.rooms.get(roomId) ?? [])];
|
|
299
|
+
}
|
|
300
|
+
/** Get all rooms */
|
|
301
|
+
getRooms() {
|
|
302
|
+
return [...this.rooms.keys()];
|
|
303
|
+
}
|
|
304
|
+
/** Broadcast to all clients */
|
|
71
305
|
broadcast(content) {
|
|
72
306
|
const msg = JSON.stringify({ type: 'broadcast', content, timestamp: Date.now() });
|
|
73
|
-
for (const
|
|
74
|
-
if (
|
|
75
|
-
|
|
307
|
+
for (const [, info] of this.clients) {
|
|
308
|
+
if (info.ws.readyState === 1) {
|
|
309
|
+
info.ws.send(msg);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** Broadcast to all clients in a room */
|
|
314
|
+
broadcastToRoom(roomId, data, excludeSessionId) {
|
|
315
|
+
const room = this.rooms.get(roomId);
|
|
316
|
+
if (!room)
|
|
317
|
+
return;
|
|
318
|
+
const msg = typeof data === 'string' ? data : JSON.stringify(data);
|
|
319
|
+
for (const sessionId of room) {
|
|
320
|
+
if (sessionId === excludeSessionId)
|
|
321
|
+
continue;
|
|
322
|
+
const client = this.clients.get(sessionId);
|
|
323
|
+
if (client && client.ws.readyState === 1) {
|
|
324
|
+
client.ws.send(msg);
|
|
76
325
|
}
|
|
77
326
|
}
|
|
78
327
|
}
|
|
328
|
+
/** Send to specific session */
|
|
329
|
+
sendToSession(sessionId, data) {
|
|
330
|
+
const client = this.clients.get(sessionId);
|
|
331
|
+
if (!client || client.ws.readyState !== 1)
|
|
332
|
+
return false;
|
|
333
|
+
client.ws.send(typeof data === 'string' ? data : JSON.stringify(data));
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
/** Get connection stats */
|
|
337
|
+
getStats() {
|
|
338
|
+
const roomDetails = {};
|
|
339
|
+
for (const [roomId, members] of this.rooms) {
|
|
340
|
+
roomDetails[roomId] = members.size;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
clients: this.clients.size,
|
|
344
|
+
rooms: this.rooms.size,
|
|
345
|
+
roomDetails,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
79
348
|
}
|
|
80
349
|
exports.WebSocketChannel = WebSocketChannel;
|
|
81
350
|
//# sourceMappingURL=websocket.js.map
|