@phuetz/code-buddy 0.1.13 → 0.1.14
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 +228 -13
- package/dist/agent/architect-mode.d.ts +11 -0
- package/dist/agent/architect-mode.js +133 -25
- package/dist/agent/architect-mode.js.map +1 -1
- package/dist/agent/codebuddy-agent.d.ts +24 -0
- package/dist/agent/codebuddy-agent.js +118 -16
- package/dist/agent/codebuddy-agent.js.map +1 -1
- package/dist/agent/execution/agent-executor.d.ts +9 -0
- package/dist/agent/execution/agent-executor.js +61 -0
- package/dist/agent/execution/agent-executor.js.map +1 -1
- package/dist/agent/message-queue.d.ts +77 -0
- package/dist/agent/message-queue.js +116 -0
- package/dist/agent/message-queue.js.map +1 -0
- package/dist/agent/middleware/auto-observation.d.ts +37 -0
- package/dist/agent/middleware/auto-observation.js +231 -0
- package/dist/agent/middleware/auto-observation.js.map +1 -0
- package/dist/agent/middleware/index.d.ts +2 -0
- package/dist/agent/middleware/index.js +1 -0
- package/dist/agent/middleware/index.js.map +1 -1
- package/dist/agent/tool-handler.js +3 -2
- package/dist/agent/tool-handler.js.map +1 -1
- package/dist/agent/types.d.ts +7 -2
- package/dist/analytics/budget-alerts.d.ts +81 -0
- package/dist/analytics/budget-alerts.js +126 -0
- package/dist/analytics/budget-alerts.js.map +1 -0
- package/dist/analytics/cost-predictor.d.ts +79 -0
- package/dist/analytics/cost-predictor.js +150 -0
- package/dist/analytics/cost-predictor.js.map +1 -0
- package/dist/analytics/index.d.ts +2 -0
- package/dist/analytics/index.js +2 -0
- package/dist/analytics/index.js.map +1 -1
- package/dist/auth/profile-manager.d.ts +205 -0
- package/dist/auth/profile-manager.js +484 -0
- package/dist/auth/profile-manager.js.map +1 -0
- package/dist/browser-automation/browser-manager.d.ts +79 -1
- package/dist/browser-automation/browser-manager.js +265 -2
- package/dist/browser-automation/browser-manager.js.map +1 -1
- package/dist/browser-automation/profile-manager.d.ts +32 -0
- package/dist/browser-automation/profile-manager.js +83 -0
- package/dist/browser-automation/profile-manager.js.map +1 -0
- package/dist/browser-automation/route-interceptor.d.ts +29 -0
- package/dist/browser-automation/route-interceptor.js +103 -0
- package/dist/browser-automation/route-interceptor.js.map +1 -0
- package/dist/browser-automation/screenshot-annotator.d.ts +23 -0
- package/dist/browser-automation/screenshot-annotator.js +86 -0
- package/dist/browser-automation/screenshot-annotator.js.map +1 -0
- package/dist/browser-automation/types.d.ts +47 -0
- package/dist/channels/discord/client.d.ts +2 -1
- package/dist/channels/discord/client.js +28 -16
- package/dist/channels/discord/client.js.map +1 -1
- package/dist/channels/google-chat/index.d.ts +210 -0
- package/dist/channels/google-chat/index.js +505 -0
- package/dist/channels/google-chat/index.js.map +1 -0
- package/dist/channels/group-security.d.ts +182 -0
- package/dist/channels/group-security.js +407 -0
- package/dist/channels/group-security.js.map +1 -0
- package/dist/channels/index.d.ts +17 -1
- package/dist/channels/index.js +16 -0
- package/dist/channels/index.js.map +1 -1
- package/dist/channels/matrix/index.d.ts +181 -0
- package/dist/channels/matrix/index.js +643 -0
- package/dist/channels/matrix/index.js.map +1 -0
- package/dist/channels/offline-queue.d.ts +92 -0
- package/dist/channels/offline-queue.js +112 -0
- package/dist/channels/offline-queue.js.map +1 -0
- package/dist/channels/reconnection-manager.d.ts +117 -0
- package/dist/channels/reconnection-manager.js +171 -0
- package/dist/channels/reconnection-manager.js.map +1 -0
- package/dist/channels/signal/index.d.ts +184 -0
- package/dist/channels/signal/index.js +488 -0
- package/dist/channels/signal/index.js.map +1 -0
- package/dist/channels/slack/client.d.ts +2 -1
- package/dist/channels/slack/client.js +30 -20
- package/dist/channels/slack/client.js.map +1 -1
- package/dist/channels/teams/index.d.ts +196 -0
- package/dist/channels/teams/index.js +477 -0
- package/dist/channels/teams/index.js.map +1 -0
- package/dist/channels/telegram/client.d.ts +3 -1
- package/dist/channels/telegram/client.js +29 -2
- package/dist/channels/telegram/client.js.map +1 -1
- package/dist/channels/webchat/index.d.ts +103 -0
- package/dist/channels/webchat/index.js +697 -0
- package/dist/channels/webchat/index.js.map +1 -0
- package/dist/channels/whatsapp/index.d.ts +105 -0
- package/dist/channels/whatsapp/index.js +533 -0
- package/dist/channels/whatsapp/index.js.map +1 -0
- package/dist/codebuddy/client.js +6 -3
- package/dist/codebuddy/client.js.map +1 -1
- package/dist/codebuddy/tool-definitions/advanced-tools.d.ts +1 -0
- package/dist/codebuddy/tool-definitions/advanced-tools.js +103 -3
- package/dist/codebuddy/tool-definitions/advanced-tools.js.map +1 -1
- package/dist/codebuddy/tool-definitions/index.d.ts +1 -1
- package/dist/codebuddy/tool-definitions/index.js +1 -1
- package/dist/codebuddy/tool-definitions/index.js.map +1 -1
- package/dist/codebuddy/tools.js +3 -1
- package/dist/codebuddy/tools.js.map +1 -1
- package/dist/commands/cli/config-command.d.ts +8 -0
- package/dist/commands/cli/config-command.js +90 -0
- package/dist/commands/cli/config-command.js.map +1 -0
- package/dist/commands/cli/openclaw-commands.d.ts +12 -0
- package/dist/commands/cli/openclaw-commands.js +446 -0
- package/dist/commands/cli/openclaw-commands.js.map +1 -0
- package/dist/commands/cli/utility-commands.js +30 -0
- package/dist/commands/cli/utility-commands.js.map +1 -1
- package/dist/commands/client-dispatcher.js +22 -2
- package/dist/commands/client-dispatcher.js.map +1 -1
- package/dist/commands/enhanced-command-handler.js +21 -2
- package/dist/commands/enhanced-command-handler.js.map +1 -1
- package/dist/commands/handlers/extra-handlers.d.ts +30 -0
- package/dist/commands/handlers/extra-handlers.js +547 -0
- package/dist/commands/handlers/extra-handlers.js.map +1 -0
- package/dist/commands/handlers/index.d.ts +1 -0
- package/dist/commands/handlers/index.js +2 -0
- package/dist/commands/handlers/index.js.map +1 -1
- package/dist/commands/slash/builtin-commands.js +41 -34
- package/dist/commands/slash/builtin-commands.js.map +1 -1
- package/dist/config/env-schema.d.ts +58 -0
- package/dist/config/env-schema.js +789 -0
- package/dist/config/env-schema.js.map +1 -0
- package/dist/config/feature-flags.js +2 -1
- package/dist/config/feature-flags.js.map +1 -1
- package/dist/context/bootstrap-loader.d.ts +48 -0
- package/dist/context/bootstrap-loader.js +123 -0
- package/dist/context/bootstrap-loader.js.map +1 -0
- package/dist/copilot/copilot-proxy.d.ts +15 -1
- package/dist/copilot/copilot-proxy.js +81 -22
- package/dist/copilot/copilot-proxy.js.map +1 -1
- package/dist/daemon/heartbeat.d.ts +112 -0
- package/dist/daemon/heartbeat.js +339 -0
- package/dist/daemon/heartbeat.js.map +1 -0
- package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
- package/dist/desktop-automation/smart-snapshot.js +28 -0
- package/dist/desktop-automation/smart-snapshot.js.map +1 -1
- package/dist/identity/identity-manager.d.ts +95 -0
- package/dist/identity/identity-manager.js +242 -0
- package/dist/identity/identity-manager.js.map +1 -0
- package/dist/index.js +147 -17
- package/dist/index.js.map +1 -1
- package/dist/integrations/github-integration.js +1 -1
- package/dist/integrations/github-integration.js.map +1 -1
- package/dist/persistence/conversation-branches.js +2 -1
- package/dist/persistence/conversation-branches.js.map +1 -1
- package/dist/persistence/session-store.d.ts +1 -1
- package/dist/persistence/session-store.js +1 -1
- package/dist/persistence/session-store.js.map +1 -1
- package/dist/sandbox/auto-sandbox.d.ts +59 -0
- package/dist/sandbox/auto-sandbox.js +145 -0
- package/dist/sandbox/auto-sandbox.js.map +1 -0
- package/dist/security/audit-logger.d.ts +127 -0
- package/dist/security/audit-logger.js +194 -0
- package/dist/security/audit-logger.js.map +1 -0
- package/dist/security/bash-allowlist/allowlist-store.js +3 -2
- package/dist/security/bash-allowlist/allowlist-store.js.map +1 -1
- package/dist/security/bash-parser.js +0 -2
- package/dist/security/bash-parser.js.map +1 -1
- package/dist/security/code-validator.d.ts +51 -0
- package/dist/security/code-validator.js +185 -0
- package/dist/security/code-validator.js.map +1 -0
- package/dist/security/dangerous-patterns.d.ts +68 -0
- package/dist/security/dangerous-patterns.js +218 -0
- package/dist/security/dangerous-patterns.js.map +1 -0
- package/dist/security/remote-approval.d.ts +65 -0
- package/dist/security/remote-approval.js +138 -0
- package/dist/security/remote-approval.js.map +1 -0
- package/dist/security/security-audit.d.ts +7 -0
- package/dist/security/security-audit.js +23 -0
- package/dist/security/security-audit.js.map +1 -1
- package/dist/security/syntax-validator.d.ts +17 -0
- package/dist/security/syntax-validator.js +292 -0
- package/dist/security/syntax-validator.js.map +1 -0
- package/dist/server/index.js +277 -2
- package/dist/server/index.js.map +1 -1
- package/dist/services/prompt-builder.js +16 -0
- package/dist/services/prompt-builder.js.map +1 -1
- package/dist/skills/hub.d.ts +231 -0
- package/dist/skills/hub.js +694 -0
- package/dist/skills/hub.js.map +1 -0
- package/dist/skills/skill-loader.js +1 -1
- package/dist/skills/skill-loader.js.map +1 -1
- package/dist/skills/skill-manager.js +2 -1
- package/dist/skills/skill-manager.js.map +1 -1
- package/dist/tools/apply-patch.d.ts +1 -0
- package/dist/tools/apply-patch.js +57 -10
- package/dist/tools/apply-patch.js.map +1 -1
- package/dist/tools/bash/bash-tool.d.ts +123 -0
- package/dist/tools/bash/bash-tool.js +549 -0
- package/dist/tools/bash/bash-tool.js.map +1 -0
- package/dist/tools/bash/command-validator.d.ts +49 -0
- package/dist/tools/bash/command-validator.js +223 -0
- package/dist/tools/bash/command-validator.js.map +1 -0
- package/dist/tools/bash/index.d.ts +7 -0
- package/dist/tools/bash/index.js +8 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/security-patterns.d.ts +44 -0
- package/dist/tools/bash/security-patterns.js +234 -0
- package/dist/tools/bash/security-patterns.js.map +1 -0
- package/dist/tools/bash/streaming-executor.d.ts +23 -0
- package/dist/tools/bash/streaming-executor.js +134 -0
- package/dist/tools/bash/streaming-executor.js.map +1 -0
- package/dist/tools/code-formatter.js +41 -27
- package/dist/tools/code-formatter.js.map +1 -1
- package/dist/tools/code-review.js +1 -1
- package/dist/tools/code-review.js.map +1 -1
- package/dist/tools/computer-control-tool.js +21 -0
- package/dist/tools/computer-control-tool.js.map +1 -1
- package/dist/tools/document-tool.js +3 -2
- package/dist/tools/document-tool.js.map +1 -1
- package/dist/tools/git-tool.d.ts +45 -0
- package/dist/tools/git-tool.js +222 -0
- package/dist/tools/git-tool.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/multi-edit.js +31 -3
- package/dist/tools/multi-edit.js.map +1 -1
- package/dist/tools/process-tool.d.ts +69 -0
- package/dist/tools/process-tool.js +222 -0
- package/dist/tools/process-tool.js.map +1 -0
- package/dist/tools/registry/git-tools.d.ts +32 -0
- package/dist/tools/registry/git-tools.js +211 -0
- package/dist/tools/registry/git-tools.js.map +1 -0
- package/dist/tools/registry/index.d.ts +2 -0
- package/dist/tools/registry/index.js +8 -0
- package/dist/tools/registry/index.js.map +1 -1
- package/dist/tools/registry/misc-tools.d.ts +32 -4
- package/dist/tools/registry/misc-tools.js +230 -90
- package/dist/tools/registry/misc-tools.js.map +1 -1
- package/dist/tools/registry/process-tools.d.ts +20 -0
- package/dist/tools/registry/process-tools.js +141 -0
- package/dist/tools/registry/process-tools.js.map +1 -0
- package/dist/tools/registry/types.d.ts +2 -0
- package/dist/ui/components/ChatInterface.js +9 -0
- package/dist/ui/components/ChatInterface.js.map +1 -1
- package/dist/utils/autonomy-manager.js +3 -2
- package/dist/utils/autonomy-manager.js.map +1 -1
- package/dist/utils/config-validation/schema.d.ts +15 -15
- package/dist/utils/confirmation-service.d.ts +16 -0
- package/dist/utils/confirmation-service.js +37 -3
- package/dist/utils/confirmation-service.js.map +1 -1
- package/dist/utils/custom-instructions.js +2 -1
- package/dist/utils/custom-instructions.js.map +1 -1
- package/dist/utils/graceful-shutdown.js +9 -9
- package/dist/utils/graceful-shutdown.js.map +1 -1
- package/dist/utils/head-tail-truncation.d.ts +18 -0
- package/dist/utils/head-tail-truncation.js +127 -0
- package/dist/utils/head-tail-truncation.js.map +1 -1
- package/dist/utils/history-manager.js +3 -2
- package/dist/utils/history-manager.js.map +1 -1
- package/dist/utils/performance.js +16 -15
- package/dist/utils/performance.js.map +1 -1
- package/dist/utils/update-notifier.js +2 -1
- package/dist/utils/update-notifier.js.map +1 -1
- package/dist/workflows/pipeline.d.ts +54 -1
- package/dist/workflows/pipeline.js +128 -7
- package/dist/workflows/pipeline.js.map +1 -1
- package/dist/workflows/step-manager.js +2 -1
- package/dist/workflows/step-manager.js.map +1 -1
- package/package.json +6 -3
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebChat Channel Adapter
|
|
3
|
+
*
|
|
4
|
+
* Built-in HTTP/WebSocket chat server that provides a browser-based
|
|
5
|
+
* chat interface. Serves a simple HTML UI and manages connected
|
|
6
|
+
* WebSocket clients for real-time bidirectional messaging.
|
|
7
|
+
*
|
|
8
|
+
* No external dependencies required -- uses Node.js built-in
|
|
9
|
+
* http module and the ws package (already a project dependency).
|
|
10
|
+
*/
|
|
11
|
+
import http from 'http';
|
|
12
|
+
import { randomUUID } from 'crypto';
|
|
13
|
+
import { BaseChannel, getSessionKey, checkDMPairing } from '../index.js';
|
|
14
|
+
import { logger } from '../../utils/logger.js';
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Channel Implementation
|
|
17
|
+
// ============================================================================
|
|
18
|
+
/**
|
|
19
|
+
* WebChat channel -- built-in HTTP/WS chat server
|
|
20
|
+
*/
|
|
21
|
+
export class WebChatChannel extends BaseChannel {
|
|
22
|
+
server = null;
|
|
23
|
+
wss = null;
|
|
24
|
+
clients = new Map();
|
|
25
|
+
messageHistory = [];
|
|
26
|
+
maxHistory = 100;
|
|
27
|
+
constructor(config) {
|
|
28
|
+
super('webchat', config);
|
|
29
|
+
// Apply defaults
|
|
30
|
+
const cfg = this.config;
|
|
31
|
+
if (cfg.port === undefined)
|
|
32
|
+
cfg.port = 3001;
|
|
33
|
+
if (!cfg.host)
|
|
34
|
+
cfg.host = '0.0.0.0';
|
|
35
|
+
if (!cfg.corsOrigins)
|
|
36
|
+
cfg.corsOrigins = ['*'];
|
|
37
|
+
if (!cfg.title)
|
|
38
|
+
cfg.title = 'Code Buddy WebChat';
|
|
39
|
+
if (cfg.maxMessageLength === undefined)
|
|
40
|
+
cfg.maxMessageLength = 4096;
|
|
41
|
+
}
|
|
42
|
+
get webChatConfig() {
|
|
43
|
+
return this.config;
|
|
44
|
+
}
|
|
45
|
+
// ==========================================================================
|
|
46
|
+
// Lifecycle
|
|
47
|
+
// ==========================================================================
|
|
48
|
+
/**
|
|
49
|
+
* Start the HTTP and WebSocket servers
|
|
50
|
+
*/
|
|
51
|
+
async connect() {
|
|
52
|
+
let WebSocketModule;
|
|
53
|
+
try {
|
|
54
|
+
WebSocketModule = await import('ws');
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new Error('WebChat channel requires the ws package. Install it with: npm install ws');
|
|
58
|
+
}
|
|
59
|
+
const port = this.webChatConfig.port ?? 3001;
|
|
60
|
+
const host = this.webChatConfig.host ?? '0.0.0.0';
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
try {
|
|
63
|
+
// Create HTTP server
|
|
64
|
+
this.server = http.createServer((req, res) => {
|
|
65
|
+
this.handleHttpRequest(req, res);
|
|
66
|
+
});
|
|
67
|
+
// Create WebSocket server attached to the HTTP server
|
|
68
|
+
const WebSocketServer = WebSocketModule.WebSocketServer;
|
|
69
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
70
|
+
this.wss.on('connection', (ws, req) => {
|
|
71
|
+
this.handleWsConnection(ws, req);
|
|
72
|
+
});
|
|
73
|
+
this.wss.on('error', (err) => {
|
|
74
|
+
logger.debug('WebChat WS server error', {
|
|
75
|
+
error: err instanceof Error ? err.message : String(err),
|
|
76
|
+
});
|
|
77
|
+
this.emit('error', 'webchat', err);
|
|
78
|
+
});
|
|
79
|
+
this.server.on('error', (err) => {
|
|
80
|
+
this.emit('error', 'webchat', err);
|
|
81
|
+
reject(err);
|
|
82
|
+
});
|
|
83
|
+
this.server.listen(port, host, () => {
|
|
84
|
+
this.status.connected = true;
|
|
85
|
+
this.status.authenticated = true;
|
|
86
|
+
this.status.info = {
|
|
87
|
+
port,
|
|
88
|
+
host,
|
|
89
|
+
url: `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`,
|
|
90
|
+
};
|
|
91
|
+
logger.debug('WebChat server started', { port, host });
|
|
92
|
+
this.emit('connected', 'webchat');
|
|
93
|
+
resolve();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
reject(error);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Stop the servers and disconnect all clients
|
|
103
|
+
*/
|
|
104
|
+
async disconnect() {
|
|
105
|
+
// Close all WebSocket connections
|
|
106
|
+
for (const [id, client] of this.clients) {
|
|
107
|
+
try {
|
|
108
|
+
client.ws.close(1000, 'Server shutting down');
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Ignore close errors
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
this.clients.clear();
|
|
115
|
+
// Close WebSocket server
|
|
116
|
+
if (this.wss) {
|
|
117
|
+
this.wss.close();
|
|
118
|
+
this.wss = null;
|
|
119
|
+
}
|
|
120
|
+
// Close HTTP server
|
|
121
|
+
if (this.server) {
|
|
122
|
+
await new Promise((resolve) => {
|
|
123
|
+
this.server.close(() => resolve());
|
|
124
|
+
});
|
|
125
|
+
this.server = null;
|
|
126
|
+
}
|
|
127
|
+
this.status.connected = false;
|
|
128
|
+
this.status.authenticated = false;
|
|
129
|
+
this.emit('disconnected', 'webchat');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Send a message to a specific client or broadcast to all
|
|
133
|
+
*/
|
|
134
|
+
async send(message) {
|
|
135
|
+
if (!this.status.connected) {
|
|
136
|
+
return { success: false, error: 'WebChat not connected', timestamp: new Date() };
|
|
137
|
+
}
|
|
138
|
+
const now = new Date();
|
|
139
|
+
const msgId = randomUUID();
|
|
140
|
+
const wsMsg = {
|
|
141
|
+
type: 'message',
|
|
142
|
+
id: msgId,
|
|
143
|
+
content: message.content,
|
|
144
|
+
user: { id: 'bot', username: 'assistant', displayName: 'Assistant', isBot: true },
|
|
145
|
+
timestamp: now.toISOString(),
|
|
146
|
+
replyTo: message.replyTo,
|
|
147
|
+
};
|
|
148
|
+
// Store in history
|
|
149
|
+
this.addToHistory({
|
|
150
|
+
id: msgId,
|
|
151
|
+
content: message.content,
|
|
152
|
+
user: wsMsg.user,
|
|
153
|
+
timestamp: now.toISOString(),
|
|
154
|
+
replyTo: message.replyTo,
|
|
155
|
+
});
|
|
156
|
+
const payload = JSON.stringify(wsMsg);
|
|
157
|
+
if (message.channelId === '*' || message.channelId === 'broadcast') {
|
|
158
|
+
// Broadcast to all connected clients
|
|
159
|
+
let sent = 0;
|
|
160
|
+
for (const [, client] of this.clients) {
|
|
161
|
+
try {
|
|
162
|
+
if (client.ws.readyState === 1) { // WebSocket.OPEN = 1
|
|
163
|
+
client.ws.send(payload);
|
|
164
|
+
sent++;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
// Individual send failures are non-fatal
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
success: sent > 0 || this.clients.size === 0,
|
|
173
|
+
messageId: msgId,
|
|
174
|
+
timestamp: now,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
// Send to specific client
|
|
178
|
+
const client = this.clients.get(message.channelId);
|
|
179
|
+
if (!client) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error: `Client ${message.channelId} not found`,
|
|
183
|
+
timestamp: now,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
if (client.ws.readyState === 1) {
|
|
188
|
+
client.ws.send(payload);
|
|
189
|
+
return { success: true, messageId: msgId, timestamp: now };
|
|
190
|
+
}
|
|
191
|
+
return { success: false, error: 'Client WebSocket not open', timestamp: now };
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
error: error instanceof Error ? error.message : String(error),
|
|
197
|
+
timestamp: now,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get number of connected clients
|
|
203
|
+
*/
|
|
204
|
+
getClientCount() {
|
|
205
|
+
return this.clients.size;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get connected client IDs
|
|
209
|
+
*/
|
|
210
|
+
getClientIds() {
|
|
211
|
+
return Array.from(this.clients.keys());
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Broadcast a system message to all clients
|
|
215
|
+
*/
|
|
216
|
+
async broadcastSystem(content) {
|
|
217
|
+
const msg = {
|
|
218
|
+
type: 'system',
|
|
219
|
+
content,
|
|
220
|
+
timestamp: new Date().toISOString(),
|
|
221
|
+
};
|
|
222
|
+
const payload = JSON.stringify(msg);
|
|
223
|
+
for (const [, client] of this.clients) {
|
|
224
|
+
try {
|
|
225
|
+
if (client.ws.readyState === 1) {
|
|
226
|
+
client.ws.send(payload);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Ignore individual failures
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// ==========================================================================
|
|
235
|
+
// HTTP Handler
|
|
236
|
+
// ==========================================================================
|
|
237
|
+
/**
|
|
238
|
+
* Handle incoming HTTP requests
|
|
239
|
+
*/
|
|
240
|
+
handleHttpRequest(req, res) {
|
|
241
|
+
const url = req.url ?? '/';
|
|
242
|
+
const corsOrigins = this.webChatConfig.corsOrigins ?? ['*'];
|
|
243
|
+
const origin = req.headers.origin ?? '*';
|
|
244
|
+
// CORS headers
|
|
245
|
+
const allowedOrigin = corsOrigins.includes('*') ? '*' : (corsOrigins.includes(origin) ? origin : '');
|
|
246
|
+
if (allowedOrigin) {
|
|
247
|
+
res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
|
|
248
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
249
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
250
|
+
}
|
|
251
|
+
if (req.method === 'OPTIONS') {
|
|
252
|
+
res.writeHead(204);
|
|
253
|
+
res.end();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (url === '/' || url === '/index.html') {
|
|
257
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
258
|
+
res.end(this.getChatHtml());
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (url === '/api/health') {
|
|
262
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
263
|
+
res.end(JSON.stringify({
|
|
264
|
+
status: 'ok',
|
|
265
|
+
clients: this.clients.size,
|
|
266
|
+
uptime: process.uptime(),
|
|
267
|
+
}));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (url === '/api/history') {
|
|
271
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
272
|
+
res.end(JSON.stringify({ messages: this.messageHistory }));
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
// 404 for everything else
|
|
276
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
277
|
+
res.end(JSON.stringify({ error: 'Not found' }));
|
|
278
|
+
}
|
|
279
|
+
// ==========================================================================
|
|
280
|
+
// WebSocket Handler
|
|
281
|
+
// ==========================================================================
|
|
282
|
+
/**
|
|
283
|
+
* Handle a new WebSocket connection
|
|
284
|
+
*/
|
|
285
|
+
handleWsConnection(ws, req) {
|
|
286
|
+
const clientId = randomUUID();
|
|
287
|
+
const ip = req.headers['x-forwarded-for'] ?? req.socket.remoteAddress ?? 'unknown';
|
|
288
|
+
// If auth token is required, wait for auth message
|
|
289
|
+
const needsAuth = !!this.webChatConfig.authToken;
|
|
290
|
+
let authenticated = !needsAuth;
|
|
291
|
+
const client = {
|
|
292
|
+
id: clientId,
|
|
293
|
+
ws,
|
|
294
|
+
user: {
|
|
295
|
+
id: clientId,
|
|
296
|
+
username: `user-${clientId.slice(0, 8)}`,
|
|
297
|
+
displayName: `User ${clientId.slice(0, 8)}`,
|
|
298
|
+
isBot: false,
|
|
299
|
+
},
|
|
300
|
+
connectedAt: new Date(),
|
|
301
|
+
lastActivity: new Date(),
|
|
302
|
+
};
|
|
303
|
+
if (!needsAuth) {
|
|
304
|
+
this.clients.set(clientId, client);
|
|
305
|
+
this.sendWelcome(client);
|
|
306
|
+
}
|
|
307
|
+
ws.on('message', async (data) => {
|
|
308
|
+
try {
|
|
309
|
+
const msg = JSON.parse(data.toString());
|
|
310
|
+
client.lastActivity = new Date();
|
|
311
|
+
// Handle auth
|
|
312
|
+
if (msg.type === 'auth') {
|
|
313
|
+
if (needsAuth && msg.token !== this.webChatConfig.authToken) {
|
|
314
|
+
ws.send(JSON.stringify({ type: 'system', content: 'Authentication failed' }));
|
|
315
|
+
ws.close(4001, 'Unauthorized');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
authenticated = true;
|
|
319
|
+
// Update user info from auth message
|
|
320
|
+
if (msg.user) {
|
|
321
|
+
client.user = {
|
|
322
|
+
...client.user,
|
|
323
|
+
...msg.user,
|
|
324
|
+
id: msg.user.id ?? clientId,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
this.clients.set(clientId, client);
|
|
328
|
+
this.sendWelcome(client);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (!authenticated) {
|
|
332
|
+
ws.send(JSON.stringify({ type: 'system', content: 'Please authenticate first' }));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// Handle message
|
|
336
|
+
if (msg.type === 'message') {
|
|
337
|
+
await this.handleWsMessage(client, msg);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
// Handle typing
|
|
341
|
+
if (msg.type === 'typing') {
|
|
342
|
+
this.emit('typing', {
|
|
343
|
+
id: clientId,
|
|
344
|
+
type: 'webchat',
|
|
345
|
+
}, client.user);
|
|
346
|
+
// Broadcast typing to other clients
|
|
347
|
+
this.broadcastExcept(clientId, JSON.stringify({
|
|
348
|
+
type: 'typing',
|
|
349
|
+
user: client.user,
|
|
350
|
+
timestamp: new Date().toISOString(),
|
|
351
|
+
}));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// Handle history request
|
|
355
|
+
if (msg.type === 'history') {
|
|
356
|
+
ws.send(JSON.stringify({
|
|
357
|
+
type: 'history',
|
|
358
|
+
messages: this.messageHistory,
|
|
359
|
+
}));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
logger.debug('WebChat: error processing WS message', {
|
|
365
|
+
clientId,
|
|
366
|
+
error: err instanceof Error ? err.message : String(err),
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
ws.on('close', () => {
|
|
371
|
+
this.clients.delete(clientId);
|
|
372
|
+
logger.debug('WebChat: client disconnected', { clientId });
|
|
373
|
+
// Notify other clients
|
|
374
|
+
this.broadcastExcept(clientId, JSON.stringify({
|
|
375
|
+
type: 'system',
|
|
376
|
+
content: `${client.user.displayName ?? clientId} has left the chat`,
|
|
377
|
+
timestamp: new Date().toISOString(),
|
|
378
|
+
}));
|
|
379
|
+
});
|
|
380
|
+
ws.on('error', (err) => {
|
|
381
|
+
logger.debug('WebChat: client error', {
|
|
382
|
+
clientId,
|
|
383
|
+
error: err instanceof Error ? err.message : String(err),
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
ws.on('pong', () => {
|
|
387
|
+
client.lastActivity = new Date();
|
|
388
|
+
});
|
|
389
|
+
logger.debug('WebChat: client connected', { clientId, ip });
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Handle an incoming WebSocket chat message
|
|
393
|
+
*/
|
|
394
|
+
async handleWsMessage(client, msg) {
|
|
395
|
+
const content = (msg.content ?? '').trim();
|
|
396
|
+
if (!content)
|
|
397
|
+
return;
|
|
398
|
+
// Enforce max message length
|
|
399
|
+
const maxLen = this.webChatConfig.maxMessageLength ?? 4096;
|
|
400
|
+
if (content.length > maxLen) {
|
|
401
|
+
client.ws.send(JSON.stringify({
|
|
402
|
+
type: 'system',
|
|
403
|
+
content: `Message too long (max ${maxLen} characters)`,
|
|
404
|
+
}));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Check user allowlist
|
|
408
|
+
if (!this.isUserAllowed(client.id))
|
|
409
|
+
return;
|
|
410
|
+
const now = new Date();
|
|
411
|
+
const msgId = msg.id ?? randomUUID();
|
|
412
|
+
// Store in history
|
|
413
|
+
this.addToHistory({
|
|
414
|
+
id: msgId,
|
|
415
|
+
content,
|
|
416
|
+
user: client.user,
|
|
417
|
+
timestamp: now.toISOString(),
|
|
418
|
+
replyTo: msg.replyTo,
|
|
419
|
+
});
|
|
420
|
+
// Broadcast the message to all other clients (echo)
|
|
421
|
+
this.broadcastExcept(client.id, JSON.stringify({
|
|
422
|
+
type: 'message',
|
|
423
|
+
id: msgId,
|
|
424
|
+
content,
|
|
425
|
+
user: client.user,
|
|
426
|
+
timestamp: now.toISOString(),
|
|
427
|
+
replyTo: msg.replyTo,
|
|
428
|
+
}));
|
|
429
|
+
// Build InboundMessage
|
|
430
|
+
const inbound = {
|
|
431
|
+
id: msgId,
|
|
432
|
+
channel: {
|
|
433
|
+
id: client.id,
|
|
434
|
+
type: 'webchat',
|
|
435
|
+
name: 'WebChat',
|
|
436
|
+
isDM: true,
|
|
437
|
+
isGroup: false,
|
|
438
|
+
},
|
|
439
|
+
sender: client.user,
|
|
440
|
+
content,
|
|
441
|
+
contentType: this.determineContentType(content, msg.attachments),
|
|
442
|
+
attachments: msg.attachments,
|
|
443
|
+
replyTo: msg.replyTo,
|
|
444
|
+
timestamp: now,
|
|
445
|
+
raw: msg,
|
|
446
|
+
};
|
|
447
|
+
const parsed = this.parseCommand(inbound);
|
|
448
|
+
parsed.sessionKey = getSessionKey(parsed);
|
|
449
|
+
// DM pairing check
|
|
450
|
+
const pairingStatus = await checkDMPairing(parsed);
|
|
451
|
+
if (!pairingStatus.approved) {
|
|
452
|
+
const { getDMPairing } = await import('../dm-pairing.js');
|
|
453
|
+
const pairingMessage = getDMPairing().getPairingMessage(pairingStatus);
|
|
454
|
+
if (pairingMessage) {
|
|
455
|
+
await this.send({ channelId: client.id, content: pairingMessage });
|
|
456
|
+
}
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
this.status.lastActivity = now;
|
|
460
|
+
this.emit('message', parsed);
|
|
461
|
+
if (parsed.isCommand) {
|
|
462
|
+
this.emit('command', parsed);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// ==========================================================================
|
|
466
|
+
// Helpers
|
|
467
|
+
// ==========================================================================
|
|
468
|
+
/**
|
|
469
|
+
* Send welcome message and history to a newly connected client
|
|
470
|
+
*/
|
|
471
|
+
sendWelcome(client) {
|
|
472
|
+
// Send system welcome
|
|
473
|
+
client.ws.send(JSON.stringify({
|
|
474
|
+
type: 'system',
|
|
475
|
+
content: `Welcome to ${this.webChatConfig.title ?? 'Code Buddy WebChat'}! You are connected as ${client.user.displayName}.`,
|
|
476
|
+
timestamp: new Date().toISOString(),
|
|
477
|
+
}));
|
|
478
|
+
// Send recent history
|
|
479
|
+
if (this.messageHistory.length > 0) {
|
|
480
|
+
client.ws.send(JSON.stringify({
|
|
481
|
+
type: 'history',
|
|
482
|
+
messages: this.messageHistory,
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
485
|
+
// Notify other clients
|
|
486
|
+
this.broadcastExcept(client.id, JSON.stringify({
|
|
487
|
+
type: 'system',
|
|
488
|
+
content: `${client.user.displayName ?? client.id} has joined the chat`,
|
|
489
|
+
timestamp: new Date().toISOString(),
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Broadcast a message to all clients except the sender
|
|
494
|
+
*/
|
|
495
|
+
broadcastExcept(excludeId, payload) {
|
|
496
|
+
for (const [id, client] of this.clients) {
|
|
497
|
+
if (id === excludeId)
|
|
498
|
+
continue;
|
|
499
|
+
try {
|
|
500
|
+
if (client.ws.readyState === 1) {
|
|
501
|
+
client.ws.send(payload);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
// Ignore individual send failures
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* Add a message to the history ring buffer
|
|
511
|
+
*/
|
|
512
|
+
addToHistory(msg) {
|
|
513
|
+
this.messageHistory.push(msg);
|
|
514
|
+
if (this.messageHistory.length > this.maxHistory) {
|
|
515
|
+
this.messageHistory.shift();
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Determine content type from message content
|
|
520
|
+
*/
|
|
521
|
+
determineContentType(content, attachments) {
|
|
522
|
+
if (attachments && attachments.length > 0)
|
|
523
|
+
return attachments[0].type;
|
|
524
|
+
if (content.startsWith('/'))
|
|
525
|
+
return 'command';
|
|
526
|
+
return 'text';
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Generate the HTML for the chat interface
|
|
530
|
+
*/
|
|
531
|
+
getChatHtml() {
|
|
532
|
+
const title = this.webChatConfig.title ?? 'Code Buddy WebChat';
|
|
533
|
+
const wsProtocol = 'ws';
|
|
534
|
+
return `<!DOCTYPE html>
|
|
535
|
+
<html lang="en">
|
|
536
|
+
<head>
|
|
537
|
+
<meta charset="utf-8">
|
|
538
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
539
|
+
<title>${this.escapeHtml(title)}</title>
|
|
540
|
+
<style>
|
|
541
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
542
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #1a1a2e; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
|
|
543
|
+
#header { padding: 12px 20px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; align-items: center; justify-content: space-between; }
|
|
544
|
+
#header h1 { font-size: 16px; color: #e94560; }
|
|
545
|
+
#header .status { font-size: 12px; color: #888; }
|
|
546
|
+
#header .status.connected { color: #4caf50; }
|
|
547
|
+
#messages { flex: 1; overflow-y: auto; padding: 16px 20px; display: flex; flex-direction: column; gap: 8px; }
|
|
548
|
+
.message { max-width: 75%; padding: 10px 14px; border-radius: 12px; line-height: 1.5; word-wrap: break-word; white-space: pre-wrap; }
|
|
549
|
+
.message.user { align-self: flex-end; background: #0f3460; color: #e0e0e0; border-bottom-right-radius: 4px; }
|
|
550
|
+
.message.bot { align-self: flex-start; background: #1a1a3e; border: 1px solid #333; border-bottom-left-radius: 4px; }
|
|
551
|
+
.message.system { align-self: center; background: transparent; color: #666; font-size: 12px; font-style: italic; }
|
|
552
|
+
.message .sender { font-size: 11px; color: #888; margin-bottom: 4px; }
|
|
553
|
+
.message .time { font-size: 10px; color: #555; margin-top: 4px; text-align: right; }
|
|
554
|
+
#input-area { padding: 12px 20px; background: #16213e; border-top: 1px solid #0f3460; display: flex; gap: 8px; }
|
|
555
|
+
#input-area input { flex: 1; padding: 10px 14px; border: 1px solid #333; border-radius: 8px; background: #1a1a2e; color: #e0e0e0; font-size: 14px; outline: none; }
|
|
556
|
+
#input-area input:focus { border-color: #e94560; }
|
|
557
|
+
#input-area button { padding: 10px 20px; border: none; border-radius: 8px; background: #e94560; color: white; font-size: 14px; cursor: pointer; }
|
|
558
|
+
#input-area button:hover { background: #c73650; }
|
|
559
|
+
#input-area button:disabled { background: #555; cursor: not-allowed; }
|
|
560
|
+
</style>
|
|
561
|
+
</head>
|
|
562
|
+
<body>
|
|
563
|
+
<div id="header">
|
|
564
|
+
<h1>${this.escapeHtml(title)}</h1>
|
|
565
|
+
<span id="status" class="status">Connecting...</span>
|
|
566
|
+
</div>
|
|
567
|
+
<div id="messages"></div>
|
|
568
|
+
<div id="input-area">
|
|
569
|
+
<input id="msg-input" type="text" placeholder="Type a message..." autocomplete="off" disabled>
|
|
570
|
+
<button id="send-btn" disabled>Send</button>
|
|
571
|
+
</div>
|
|
572
|
+
<script>
|
|
573
|
+
(function() {
|
|
574
|
+
const messagesEl = document.getElementById('messages');
|
|
575
|
+
const inputEl = document.getElementById('msg-input');
|
|
576
|
+
const sendBtn = document.getElementById('send-btn');
|
|
577
|
+
const statusEl = document.getElementById('status');
|
|
578
|
+
let ws = null;
|
|
579
|
+
let reconnectTimer = null;
|
|
580
|
+
let typingTimer = null;
|
|
581
|
+
|
|
582
|
+
function connect() {
|
|
583
|
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
584
|
+
ws = new WebSocket(proto + '://' + location.host);
|
|
585
|
+
|
|
586
|
+
ws.onopen = function() {
|
|
587
|
+
statusEl.textContent = 'Connected';
|
|
588
|
+
statusEl.className = 'status connected';
|
|
589
|
+
inputEl.disabled = false;
|
|
590
|
+
sendBtn.disabled = false;
|
|
591
|
+
inputEl.focus();
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
ws.onclose = function() {
|
|
595
|
+
statusEl.textContent = 'Disconnected';
|
|
596
|
+
statusEl.className = 'status';
|
|
597
|
+
inputEl.disabled = true;
|
|
598
|
+
sendBtn.disabled = true;
|
|
599
|
+
reconnectTimer = setTimeout(connect, 3000);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
ws.onerror = function() {
|
|
603
|
+
statusEl.textContent = 'Error';
|
|
604
|
+
statusEl.className = 'status';
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
ws.onmessage = function(event) {
|
|
608
|
+
try {
|
|
609
|
+
const msg = JSON.parse(event.data);
|
|
610
|
+
if (msg.type === 'history' && msg.messages) {
|
|
611
|
+
msg.messages.forEach(function(m) { addMessage(m.content, m.user, m.timestamp); });
|
|
612
|
+
scrollBottom();
|
|
613
|
+
} else if (msg.type === 'message') {
|
|
614
|
+
addMessage(msg.content, msg.user, msg.timestamp);
|
|
615
|
+
scrollBottom();
|
|
616
|
+
} else if (msg.type === 'system') {
|
|
617
|
+
addSystemMessage(msg.content);
|
|
618
|
+
scrollBottom();
|
|
619
|
+
}
|
|
620
|
+
} catch(e) { console.error('Parse error', e); }
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function addMessage(content, user, timestamp) {
|
|
625
|
+
const div = document.createElement('div');
|
|
626
|
+
const isBot = user && user.isBot;
|
|
627
|
+
div.className = 'message ' + (isBot ? 'bot' : 'user');
|
|
628
|
+
let html = '';
|
|
629
|
+
if (user && user.displayName) {
|
|
630
|
+
html += '<div class="sender">' + escapeHtml(user.displayName) + '</div>';
|
|
631
|
+
}
|
|
632
|
+
html += escapeHtml(content);
|
|
633
|
+
if (timestamp) {
|
|
634
|
+
const t = new Date(timestamp);
|
|
635
|
+
html += '<div class="time">' + t.toLocaleTimeString() + '</div>';
|
|
636
|
+
}
|
|
637
|
+
div.innerHTML = html;
|
|
638
|
+
messagesEl.appendChild(div);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function addSystemMessage(content) {
|
|
642
|
+
const div = document.createElement('div');
|
|
643
|
+
div.className = 'message system';
|
|
644
|
+
div.textContent = content;
|
|
645
|
+
messagesEl.appendChild(div);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function scrollBottom() {
|
|
649
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function sendMessage() {
|
|
653
|
+
const text = inputEl.value.trim();
|
|
654
|
+
if (!text || !ws || ws.readyState !== 1) return;
|
|
655
|
+
ws.send(JSON.stringify({ type: 'message', content: text }));
|
|
656
|
+
addMessage(text, { displayName: 'You', isBot: false }, new Date().toISOString());
|
|
657
|
+
scrollBottom();
|
|
658
|
+
inputEl.value = '';
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function escapeHtml(str) {
|
|
662
|
+
if (!str) return '';
|
|
663
|
+
return str.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
667
|
+
inputEl.addEventListener('keydown', function(e) {
|
|
668
|
+
if (e.key === 'Enter') sendMessage();
|
|
669
|
+
// Send typing indicator
|
|
670
|
+
if (ws && ws.readyState === 1) {
|
|
671
|
+
clearTimeout(typingTimer);
|
|
672
|
+
typingTimer = setTimeout(function() {
|
|
673
|
+
ws.send(JSON.stringify({ type: 'typing' }));
|
|
674
|
+
}, 300);
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
connect();
|
|
679
|
+
})();
|
|
680
|
+
</script>
|
|
681
|
+
</body>
|
|
682
|
+
</html>`;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Escape HTML entities
|
|
686
|
+
*/
|
|
687
|
+
escapeHtml(str) {
|
|
688
|
+
return str
|
|
689
|
+
.replace(/&/g, '&')
|
|
690
|
+
.replace(/</g, '<')
|
|
691
|
+
.replace(/>/g, '>')
|
|
692
|
+
.replace(/"/g, '"')
|
|
693
|
+
.replace(/'/g, ''');
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
export default WebChatChannel;
|
|
697
|
+
//# sourceMappingURL=index.js.map
|