agentchannel 0.5.0 → 0.5.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/dist/web.js +153 -108
- package/dist/web.js.map +1 -1
- package/package.json +1 -1
package/dist/web.js
CHANGED
|
@@ -1,160 +1,168 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
import mqtt from "mqtt";
|
|
3
|
+
import { deriveKey, hashRoom, decrypt } from "./crypto.js";
|
|
4
|
+
const MAX_HISTORY = 200;
|
|
5
|
+
const CSS = `*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
|
6
|
+
:root{--bg-primary:#1a1a2e;--bg-secondary:#16213e;--bg-hover:#1f2b47;--text-primary:#e0e0e0;--text-secondary:#8892a4;--text-muted:#565f73;--mention-bg:rgba(88,133,240,0.15);--mention-text:#7cacf8;--mention-border:rgba(88,133,240,0.3);--system-text:#565f73;--channel-bg:rgba(77,186,135,0.12);--channel-text:#4dba87;--channel-border:rgba(77,186,135,0.25);--divider:rgba(255,255,255,0.06);--font-sans:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;--radius-sm:4px;--radius-lg:12px}
|
|
7
|
+
html{font-size:15px;-webkit-font-smoothing:antialiased}
|
|
8
|
+
body{font-family:var(--font-sans);background:var(--bg-primary);color:var(--text-primary);line-height:1.6;min-height:100vh}
|
|
9
|
+
.chat-container{max-width:820px;margin:0 auto;display:flex;flex-direction:column;min-height:100vh}
|
|
10
|
+
.chat-header{padding:20px 20px 16px;border-bottom:1px solid var(--divider);display:flex;align-items:baseline;gap:12px;position:sticky;top:0;background:var(--bg-primary);z-index:10}
|
|
11
|
+
.chat-title{font-size:1.15rem;font-weight:700;letter-spacing:-0.01em}
|
|
12
|
+
.chat-status{font-size:0.8rem;color:var(--text-muted)}
|
|
13
|
+
.chat-status.connected{color:#4ade80}
|
|
14
|
+
.message-list{padding:8px 0;flex:1}
|
|
15
|
+
.message{display:flex;padding:2px 20px;transition:background-color 0.1s ease}
|
|
16
|
+
.message:hover{background:var(--bg-hover)}
|
|
17
|
+
.message__gutter{width:40px;flex-shrink:0;margin-right:12px;display:flex;align-items:flex-start;justify-content:center;padding-top:4px}
|
|
18
|
+
.message__avatar{width:36px;height:36px;border-radius:var(--radius-lg);background:var(--avatar-color,#7c6bf0);color:#fff;font-weight:700;font-size:0.85rem;display:flex;align-items:center;justify-content:center;user-select:none}
|
|
19
|
+
.message__body{flex:1;min-width:0}
|
|
20
|
+
.message--first{padding-top:10px;margin-top:4px}
|
|
21
|
+
.message--first+.message--first{border-top:1px solid var(--divider)}
|
|
22
|
+
.message--system+.message--first,.message--first:first-child{border-top:none}
|
|
23
|
+
.message__header{display:flex;align-items:baseline;gap:8px;margin-bottom:2px;flex-wrap:wrap}
|
|
24
|
+
.message__sender{font-weight:700;font-size:0.935rem;color:var(--sender-color,#7c6bf0)}
|
|
25
|
+
.message__channel{display:inline-block;font-size:0.72rem;font-weight:600;padding:1px 7px;border-radius:9999px;background:var(--channel-bg);color:var(--channel-text);border:1px solid var(--channel-border);white-space:nowrap}
|
|
26
|
+
.message__time{font-size:0.72rem;color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
27
|
+
.message__time--hover{opacity:0;font-size:0.68rem;transition:opacity 0.15s ease;padding-top:4px}
|
|
28
|
+
.message--grouped:hover .message__time--hover{opacity:1}
|
|
29
|
+
.message__content{font-size:0.935rem;line-height:1.65;word-wrap:break-word}
|
|
30
|
+
.message--grouped{padding-top:1px;padding-bottom:1px}
|
|
31
|
+
.mention{display:inline;background:var(--mention-bg);color:var(--mention-text);padding:1px 5px;border-radius:var(--radius-sm);font-weight:600;font-size:0.9rem;border:1px solid var(--mention-border);white-space:nowrap}
|
|
32
|
+
.message--system{padding-top:6px;padding-bottom:6px}
|
|
33
|
+
.message--system .message__body{display:flex;align-items:center;gap:10px;padding-left:52px}
|
|
34
|
+
.message__system-text{font-size:0.82rem;color:var(--system-text);font-style:italic}
|
|
35
|
+
.message--system:hover{background:transparent}
|
|
36
|
+
::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:transparent}::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.1);border-radius:3px}
|
|
37
|
+
@media(max-width:600px){html{font-size:14px}.message{padding-left:12px;padding-right:12px}.message__gutter{width:32px;margin-right:10px}.message__avatar{width:30px;height:30px;font-size:0.75rem}.message--system .message__body{padding-left:42px}}`;
|
|
2
38
|
const HTML = `<!DOCTYPE html>
|
|
3
|
-
<html>
|
|
39
|
+
<html lang="en">
|
|
4
40
|
<head>
|
|
5
41
|
<meta charset="utf-8">
|
|
42
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
43
|
<title>AgentChannel</title>
|
|
7
|
-
<style>
|
|
8
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
-
body { font-family: -apple-system, system-ui, sans-serif; background: #1a1a2e; color: #e0e0e0; height: 100vh; display: flex; flex-direction: column; }
|
|
10
|
-
header { padding: 12px 20px; background: #16213e; border-bottom: 1px solid #0f3460; display: flex; align-items: center; gap: 12px; }
|
|
11
|
-
header h1 { font-size: 16px; color: #e94560; }
|
|
12
|
-
header .status { font-size: 12px; color: #4ade80; }
|
|
13
|
-
#messages { flex: 1; overflow-y: auto; padding: 16px 20px; }
|
|
14
|
-
.msg { margin-bottom: 8px; line-height: 1.5; }
|
|
15
|
-
.msg .time { color: #666; font-size: 12px; }
|
|
16
|
-
.msg .channel { color: #e94560; }
|
|
17
|
-
.msg .sender { color: #4ade80; font-weight: bold; }
|
|
18
|
-
.msg .content { color: #e0e0e0; }
|
|
19
|
-
.msg.system { color: #666; font-style: italic; }
|
|
20
|
-
.msg.mention { background: #2a2a4a; border-left: 3px solid #e94560; padding: 4px 8px; }
|
|
21
|
-
#input-area { padding: 12px 20px; background: #16213e; border-top: 1px solid #0f3460; display: flex; gap: 8px; }
|
|
22
|
-
#channel-select { background: #1a1a2e; color: #e94560; border: 1px solid #0f3460; padding: 8px; border-radius: 4px; }
|
|
23
|
-
#msg-input { flex: 1; background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; padding: 8px 12px; border-radius: 4px; font-size: 14px; }
|
|
24
|
-
#msg-input:focus { outline: none; border-color: #e94560; }
|
|
25
|
-
#send-btn { background: #e94560; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; }
|
|
26
|
-
</style>
|
|
44
|
+
<style>${CSS}</style>
|
|
27
45
|
</head>
|
|
28
46
|
<body>
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
<div
|
|
35
|
-
<select id="channel-select"></select>
|
|
36
|
-
<input id="msg-input" placeholder="Type a message..." autocomplete="off">
|
|
37
|
-
<button id="send-btn">Send</button>
|
|
47
|
+
<div class="chat-container">
|
|
48
|
+
<header class="chat-header">
|
|
49
|
+
<h1 class="chat-title">AgentChannel</h1>
|
|
50
|
+
<span class="chat-status" id="status">connecting...</span>
|
|
51
|
+
</header>
|
|
52
|
+
<div class="message-list" id="messages"></div>
|
|
38
53
|
</div>
|
|
39
54
|
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
|
40
55
|
<script>
|
|
41
56
|
const CONFIG = __CONFIG__;
|
|
57
|
+
const COLORS = ["#7c6bf0","#e06c58","#4dba87","#e0a84d","#58b4e0","#e05898","#58e0b4","#b458e0"];
|
|
58
|
+
const senderColors = {};
|
|
59
|
+
let lastSender = null;
|
|
42
60
|
|
|
43
61
|
const encoder = new TextEncoder();
|
|
44
62
|
const decoder = new TextDecoder();
|
|
45
63
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
function getSenderColor(name) {
|
|
65
|
+
if (!senderColors[name]) {
|
|
66
|
+
const idx = Object.keys(senderColors).length % COLORS.length;
|
|
67
|
+
senderColors[name] = COLORS[idx];
|
|
68
|
+
}
|
|
69
|
+
return senderColors[name];
|
|
52
70
|
}
|
|
53
71
|
|
|
54
|
-
async function
|
|
55
|
-
const
|
|
56
|
-
return
|
|
72
|
+
async function deriveKey(secret) {
|
|
73
|
+
const km = await crypto.subtle.importKey("raw", encoder.encode(secret), "PBKDF2", false, ["deriveKey"]);
|
|
74
|
+
return crypto.subtle.deriveKey({name:"PBKDF2",salt:encoder.encode("agentchannel-v1"),iterations:100000,hash:"SHA-256"},km,{name:"AES-GCM",length:256},false,["encrypt","decrypt"]);
|
|
57
75
|
}
|
|
58
76
|
|
|
59
|
-
async function
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
const buf = new Uint8Array(ct);
|
|
63
|
-
const tag = buf.slice(-16);
|
|
64
|
-
const data = buf.slice(0, -16);
|
|
65
|
-
return JSON.stringify({
|
|
66
|
-
iv: btoa(String.fromCharCode(...iv)),
|
|
67
|
-
data: btoa(String.fromCharCode(...data)),
|
|
68
|
-
tag: btoa(String.fromCharCode(...tag))
|
|
69
|
-
});
|
|
77
|
+
async function hashRoom(code) {
|
|
78
|
+
const h = await crypto.subtle.digest("SHA-256", encoder.encode(code));
|
|
79
|
+
return Array.from(new Uint8Array(h)).map(b=>b.toString(16).padStart(2,"0")).join("").slice(0,16);
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
async function decrypt(payload, key) {
|
|
73
83
|
const p = JSON.parse(payload);
|
|
74
|
-
const iv = Uint8Array.from(atob(p.iv),
|
|
75
|
-
const data = Uint8Array.from(atob(p.data),
|
|
76
|
-
const tag = Uint8Array.from(atob(p.tag),
|
|
77
|
-
const combined = new Uint8Array(data.length
|
|
84
|
+
const iv = Uint8Array.from(atob(p.iv),c=>c.charCodeAt(0));
|
|
85
|
+
const data = Uint8Array.from(atob(p.data),c=>c.charCodeAt(0));
|
|
86
|
+
const tag = Uint8Array.from(atob(p.tag),c=>c.charCodeAt(0));
|
|
87
|
+
const combined = new Uint8Array(data.length+tag.length);
|
|
78
88
|
combined.set(data); combined.set(tag, data.length);
|
|
79
|
-
const pt = await crypto.subtle.decrypt({
|
|
89
|
+
const pt = await crypto.subtle.decrypt({name:"AES-GCM",iv},key,combined);
|
|
80
90
|
return decoder.decode(pt);
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
const msgsEl = document.getElementById("messages");
|
|
94
|
+
const statusEl = document.getElementById("status");
|
|
95
|
+
|
|
96
|
+
function escapeHtml(s) { return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
|
|
97
|
+
|
|
98
|
+
function highlightMentions(text) {
|
|
99
|
+
return escapeHtml(text).replace(/@(\\w+)/g, '<span class="mention">@$1</span>');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function addMessage(channel, msg) {
|
|
103
|
+
if (msg.type === "system") {
|
|
104
|
+
const div = document.createElement("div");
|
|
105
|
+
div.className = "message message--system";
|
|
106
|
+
const time = new Date(msg.timestamp).toLocaleTimeString([], {hour:"2-digit",minute:"2-digit"});
|
|
107
|
+
div.innerHTML = '<div class="message__body"><time class="message__time">'+time+'</time><span class="message__system-text">'+escapeHtml(msg.content)+'</span></div>';
|
|
108
|
+
msgsEl.appendChild(div);
|
|
109
|
+
lastSender = null;
|
|
110
|
+
} else {
|
|
111
|
+
const isGrouped = lastSender === msg.sender;
|
|
112
|
+
const color = getSenderColor(msg.sender);
|
|
113
|
+
const time = new Date(msg.timestamp).toLocaleTimeString([], {hour:"2-digit",minute:"2-digit"});
|
|
114
|
+
const div = document.createElement("div");
|
|
115
|
+
|
|
116
|
+
if (isGrouped) {
|
|
117
|
+
div.className = "message message--grouped";
|
|
118
|
+
div.innerHTML = '<div class="message__gutter"><time class="message__time message__time--hover">'+time+'</time></div><div class="message__body"><div class="message__content">'+highlightMentions(msg.content)+'</div></div>';
|
|
119
|
+
} else {
|
|
120
|
+
div.className = "message message--first";
|
|
121
|
+
div.innerHTML = '<div class="message__gutter"><div class="message__avatar" style="--avatar-color:'+color+'">'+msg.sender[0].toUpperCase()+'</div></div><div class="message__body"><div class="message__header"><span class="message__sender" style="--sender-color:'+color+'">@'+escapeHtml(msg.sender)+'</span><span class="message__channel">#'+escapeHtml(channel)+'</span><time class="message__time">'+time+'</time></div><div class="message__content">'+highlightMentions(msg.content)+'</div></div>';
|
|
122
|
+
}
|
|
123
|
+
msgsEl.appendChild(div);
|
|
124
|
+
lastSender = msg.sender;
|
|
125
|
+
}
|
|
126
|
+
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
127
|
+
}
|
|
128
|
+
|
|
83
129
|
async function init() {
|
|
130
|
+
// Load history
|
|
131
|
+
try {
|
|
132
|
+
const res = await fetch("/api/history");
|
|
133
|
+
const history = await res.json();
|
|
134
|
+
for (const msg of history) addMessage(msg.channel, msg);
|
|
135
|
+
} catch(e) {}
|
|
136
|
+
|
|
84
137
|
const channels = {};
|
|
85
138
|
for (const ch of CONFIG.channels) {
|
|
86
|
-
channels[ch.channel] = {
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Populate channel selector
|
|
90
|
-
const sel = document.getElementById("channel-select");
|
|
91
|
-
for (const name of Object.keys(channels)) {
|
|
92
|
-
const opt = document.createElement("option");
|
|
93
|
-
opt.value = name; opt.textContent = "#" + name;
|
|
94
|
-
sel.appendChild(opt);
|
|
139
|
+
channels[ch.channel] = {key: await deriveKey(ch.key), hash: await hashRoom(ch.key), name: ch.channel};
|
|
95
140
|
}
|
|
96
141
|
|
|
97
142
|
const client = mqtt.connect("wss://broker.emqx.io:8084/mqtt");
|
|
98
|
-
const statusEl = document.getElementById("status");
|
|
99
|
-
const msgsEl = document.getElementById("messages");
|
|
100
|
-
|
|
101
143
|
client.on("connect", () => {
|
|
102
144
|
statusEl.textContent = "connected";
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
145
|
+
statusEl.className = "chat-status connected";
|
|
146
|
+
for (const ch of Object.values(channels)) client.subscribe("agentchannel/"+ch.hash+"/messages");
|
|
106
147
|
});
|
|
107
|
-
|
|
108
|
-
client.on("close", () => { statusEl.textContent = "disconnected"; });
|
|
148
|
+
client.on("close", () => { statusEl.textContent = "disconnected"; statusEl.className = "chat-status"; });
|
|
109
149
|
|
|
110
150
|
if (Notification.permission === "default") Notification.requestPermission();
|
|
111
151
|
|
|
112
152
|
client.on("message", async (topic, payload) => {
|
|
113
153
|
for (const ch of Object.values(channels)) {
|
|
114
|
-
if (topic === "agentchannel/"
|
|
154
|
+
if (topic === "agentchannel/"+ch.hash+"/messages") {
|
|
115
155
|
try {
|
|
116
156
|
const decrypted = await decrypt(payload.toString(), ch.key);
|
|
117
157
|
const msg = JSON.parse(decrypted);
|
|
118
158
|
addMessage(ch.name, msg);
|
|
119
159
|
if (msg.sender !== CONFIG.name && Notification.permission === "granted") {
|
|
120
|
-
new Notification("AgentChannel #"
|
|
160
|
+
new Notification("AgentChannel #"+ch.name, {body:"@"+msg.sender+": "+msg.content});
|
|
121
161
|
}
|
|
122
162
|
} catch(e) {}
|
|
123
163
|
}
|
|
124
164
|
}
|
|
125
165
|
});
|
|
126
|
-
|
|
127
|
-
// Send
|
|
128
|
-
async function sendMsg() {
|
|
129
|
-
const input = document.getElementById("msg-input");
|
|
130
|
-
const text = input.value.trim();
|
|
131
|
-
if (!text) return;
|
|
132
|
-
const chName = sel.value;
|
|
133
|
-
const ch = channels[chName];
|
|
134
|
-
const msg = { id: Math.random().toString(16).slice(2), channel: chName, sender: CONFIG.name, content: text, timestamp: Date.now(), type: "chat" };
|
|
135
|
-
const encrypted = await encrypt(JSON.stringify(msg), ch.key);
|
|
136
|
-
client.publish("agentchannel/" + ch.hash + "/messages", encrypted);
|
|
137
|
-
input.value = "";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
document.getElementById("send-btn").onclick = sendMsg;
|
|
141
|
-
document.getElementById("msg-input").onkeydown = (e) => { if (e.key === "Enter") sendMsg(); };
|
|
142
|
-
|
|
143
|
-
function addMessage(channel, msg) {
|
|
144
|
-
const div = document.createElement("div");
|
|
145
|
-
const time = new Date(msg.timestamp).toLocaleTimeString();
|
|
146
|
-
const isMention = msg.content.includes("@" + CONFIG.name);
|
|
147
|
-
div.className = "msg" + (msg.type === "system" ? " system" : "") + (isMention ? " mention" : "");
|
|
148
|
-
if (msg.type === "system") {
|
|
149
|
-
div.innerHTML = '<span class="time">' + time + '</span> ' + msg.content;
|
|
150
|
-
} else {
|
|
151
|
-
div.innerHTML = '<span class="time">' + time + '</span> <span class="channel">#' + channel + '</span> <span class="sender">@' + msg.sender + '</span> <span class="content">' + escapeHtml(msg.content) + '</span>';
|
|
152
|
-
}
|
|
153
|
-
msgsEl.appendChild(div);
|
|
154
|
-
msgsEl.scrollTop = msgsEl.scrollHeight;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function escapeHtml(s) { return s.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); }
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
init();
|
|
@@ -162,10 +170,47 @@ init();
|
|
|
162
170
|
</body>
|
|
163
171
|
</html>`;
|
|
164
172
|
export function startWebUI(config, port = 3456) {
|
|
173
|
+
const history = [];
|
|
174
|
+
const channelStates = config.channels.map((ch) => ({
|
|
175
|
+
channel: ch.channel,
|
|
176
|
+
key: deriveKey(ch.key),
|
|
177
|
+
hash: hashRoom(ch.key),
|
|
178
|
+
}));
|
|
179
|
+
// Server-side MQTT to collect history
|
|
180
|
+
const mqttClient = mqtt.connect("mqtt://broker.emqx.io:1883");
|
|
181
|
+
mqttClient.on("connect", () => {
|
|
182
|
+
for (const cs of channelStates) {
|
|
183
|
+
mqttClient.subscribe(`agentchannel/${cs.hash}/messages`);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
mqttClient.on("message", (_topic, payload) => {
|
|
187
|
+
for (const cs of channelStates) {
|
|
188
|
+
if (_topic === `agentchannel/${cs.hash}/messages`) {
|
|
189
|
+
try {
|
|
190
|
+
const encrypted = JSON.parse(payload.toString());
|
|
191
|
+
const decrypted = decrypt(encrypted, cs.key);
|
|
192
|
+
const msg = JSON.parse(decrypted);
|
|
193
|
+
msg.channel = cs.channel;
|
|
194
|
+
history.push(msg);
|
|
195
|
+
if (history.length > MAX_HISTORY)
|
|
196
|
+
history.shift();
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
// ignore
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
165
204
|
const html = HTML.replace("__CONFIG__", JSON.stringify(config));
|
|
166
205
|
const server = createServer((req, res) => {
|
|
167
|
-
|
|
168
|
-
|
|
206
|
+
if (req.url === "/api/history") {
|
|
207
|
+
res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
|
|
208
|
+
res.end(JSON.stringify(history));
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
212
|
+
res.end(html);
|
|
213
|
+
}
|
|
169
214
|
});
|
|
170
215
|
server.listen(port, () => {
|
|
171
216
|
console.log(`AgentChannel Web UI: http://localhost:${port}`);
|
package/dist/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../src/web.ts"],"names":[],"mappings":"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"}
|