hermes-web-ui 0.3.6 → 0.3.8
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 +21 -2
- package/bin/hermes-web-ui.mjs +15 -1
- package/dist/client/assets/{Add-CKf6ViXR.js → Add-s316k4Av.js} +1 -1
- package/dist/client/assets/{Button-CrrCCorI.js → Button-BkA_RI8a.js} +1 -1
- package/dist/client/assets/{ChannelsView-D4I7hhZO.js → ChannelsView-DPmPn8DW.js} +1 -1
- package/dist/client/assets/ChatView-DEtziOPB.js +127 -0
- package/dist/client/assets/{ChatView-Vfi_jEpI.css → ChatView-DI3XN8vz.css} +1 -1
- package/dist/client/assets/{Close-C9xwy-pW.js → Close-CrLD0IXG.js} +1 -1
- package/dist/client/assets/{FormItem-BgJdrTW0.js → FormItem-CQLdFrl9.js} +1 -1
- package/dist/client/assets/{GatewaysView-Cib2JydO.js → GatewaysView-CC1Y0tZZ.js} +1 -1
- package/dist/client/assets/{Input-ChENEW-Z.js → Input-nXKlujwJ.js} +1 -1
- package/dist/client/assets/{InputNumber-Xd6HWSdp.js → InputNumber-DLZwwIyX.js} +1 -1
- package/dist/client/assets/{JobsView-SnToCbDd.js → JobsView-BC0bBrJO.js} +2 -2
- package/dist/client/assets/{LoginView-BZdmMnsf.js → LoginView-PqpFR9bV.js} +1 -1
- package/dist/client/assets/{LogsView-DblvOJIg.js → LogsView-DtR88N0b.js} +1 -1
- package/dist/client/assets/{MarkdownRenderer-DJLVk7ei.js → MarkdownRenderer-BSLfTurm.js} +1 -1
- package/dist/client/assets/{MemoryView-exXvRwCc.js → MemoryView-CJRWnePL.js} +1 -1
- package/dist/client/assets/{Modal-B2zvXTrk.js → Modal-Bp9RK8LZ.js} +1 -1
- package/dist/client/assets/ModelsView-7Obe34Cz.js +1 -0
- package/dist/client/assets/{ModelsView-jbgZP3YF.css → ModelsView-VM-oMq5M.css} +1 -1
- package/dist/client/assets/{Popconfirm-BoZc0kKk.js → Popconfirm-DTdUi7r_.js} +1 -1
- package/dist/client/assets/{Popover-Cu52vG3D.js → Popover-_M3o0B7L.js} +1 -1
- package/dist/client/assets/{ProfilesView-D0FY7Jwe.js → ProfilesView-1_GmRx-S.js} +1 -1
- package/dist/client/assets/{Select-BHc7u-Yf.js → Select-aHPR3urY.js} +2 -2
- package/dist/client/assets/{SettingRow-i-UXlco7.js → SettingRow-DKasLuS5.js} +1 -1
- package/dist/client/assets/{SettingsView-Dhr2wzAB.css → SettingsView-C3sd8K0e.css} +1 -1
- package/dist/client/assets/SettingsView-DZCA7_CM.js +352 -0
- package/dist/client/assets/{SkillsView-B5QBaAzi.js → SkillsView-Dk7O05cK.js} +1 -1
- package/dist/client/assets/{Spin-DsNCRPk9.js → Spin-Bt_9cTiO.js} +1 -1
- package/dist/client/assets/{Suffix-3xK0KZGt.js → Suffix-XaH8SDbR.js} +1 -1
- package/dist/client/assets/{Switch-Bf63XXgA.js → Switch-D1_psmjT.js} +1 -1
- package/dist/client/assets/{Tag-Dmbj68Ki.js → Tag-3FaOhoJN.js} +1 -1
- package/dist/client/assets/{TerminalView-DrJHZ0qI.js → TerminalView-DNU7oQxK.js} +1 -1
- package/dist/client/assets/{Tooltip-CRbZNhG0.js → Tooltip-YHrHWGPa.js} +1 -1
- package/dist/client/assets/{UsageView-DQ43JasX.js → UsageView-COCrOiiV.js} +1 -1
- package/dist/client/assets/{Warning-kBbRMAif.js → Warning-B6CM9aBl.js} +1 -1
- package/dist/client/assets/{_plugin-vue_export-helper-CnosYBkx.js → _plugin-vue_export-helper-BGG8ORDx.js} +1 -1
- package/dist/client/assets/app-B7ktf7Fh.js +1 -0
- package/dist/client/assets/app-BPvTl2-V.js +1 -0
- package/dist/client/assets/{browser-Djp4tkp3.js → browser-f5W8abIG.js} +1 -1
- package/dist/client/assets/chat-6q6pkzEW.js +6 -0
- package/dist/client/assets/composables-xV7dhNpf.js +1 -0
- package/dist/client/assets/{fade-in.cssr-CIVyTG6A.js → fade-in.cssr-ifHK7yH1.js} +1 -1
- package/dist/client/assets/index-CSCYx7ux.js +284 -0
- package/dist/client/assets/{jobs-CcVaCGMJ.js → jobs-DObWfhbO.js} +1 -1
- package/dist/client/assets/{light-D9G2GshF.js → light-CxjyoF0s.js} +1 -1
- package/dist/client/assets/{light-KCEDTUGE.js → light-D1yfed_s.js} +1 -1
- package/dist/client/assets/{light-BPqyaxve.js → light-DWy-mwyK.js} +1 -1
- package/dist/client/assets/{light-CSp9-LhE.js → light-D_3MwJj1.js} +1 -1
- package/dist/client/assets/{light-BF6E9z0k.js → light-DgLcPjgU.js} +1 -1
- package/dist/client/assets/{light-BJ96fCLC.js → light-TGFKT-UB.js} +1 -1
- package/dist/client/assets/models-DQ4CT-vv.js +1 -0
- package/dist/client/assets/{pinia-iHE5_ZXa.js → pinia-DcAkZ8vx.js} +1 -1
- package/dist/client/assets/{profiles-CJCR84uQ.js → profiles-DzkigJwq.js} +1 -1
- package/dist/client/assets/{router-C-NNJUuf.js → router-D8sJ39Io.js} +2 -2
- package/dist/client/assets/{sessions-C4bnNvzS.js → sessions-Dg8n9PBo.js} +1 -1
- package/dist/client/assets/{skills-B4slZfeZ.js → skills-BehzdECn.js} +1 -1
- package/dist/client/assets/{use-message-BIpqgDet.js → use-message-DBz2JSTt.js} +1 -1
- package/dist/client/assets/{useTheme-B78N9tyz.js → useTheme-UdVT814n.js} +1 -1
- package/dist/client/index.html +27 -27
- package/dist/server/index.js +8 -3
- package/dist/server/routes/hermes/filesystem.js +231 -199
- package/dist/server/routes/hermes/group-chat.d.ts +2 -0
- package/dist/server/routes/hermes/group-chat.js +135 -0
- package/dist/server/routes/hermes/logs.js +50 -11
- package/dist/server/routes/hermes/proxy-handler.js +16 -6
- package/dist/server/routes/upload.js +41 -11
- package/dist/server/services/group-chat/coordinator.d.ts +14 -0
- package/dist/server/services/group-chat/coordinator.js +230 -0
- package/dist/server/services/group-chat/index.d.ts +5 -0
- package/dist/server/services/group-chat/index.js +115 -0
- package/dist/server/services/group-chat/rooms-db.d.ts +56 -0
- package/dist/server/services/group-chat/rooms-db.js +199 -0
- package/dist/server/services/hermes/gateway-manager.d.ts +2 -0
- package/dist/server/services/hermes/gateway-manager.js +15 -0
- package/dist/server/services/hermes/hermes-profile.d.ts +6 -0
- package/dist/server/services/hermes/hermes-profile.js +12 -0
- package/dist/server/shared/providers.js +1 -13
- package/package.json +1 -1
- package/dist/client/assets/ChatView-DxyBUK57.js +0 -127
- package/dist/client/assets/ModelsView-DGs47Cj4.js +0 -1
- package/dist/client/assets/SettingsView-BW6ctYG5.js +0 -352
- package/dist/client/assets/app-BT9yU6N6.js +0 -1
- package/dist/client/assets/app-CjNVVG5x.js +0 -1
- package/dist/client/assets/chat-DlC9S9DK.js +0 -6
- package/dist/client/assets/composables-DCA4Yga5.js +0 -1
- package/dist/client/assets/index-D12ukDT7.js +0 -284
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.groupChatRoutes = void 0;
|
|
40
|
+
const router_1 = __importDefault(require("@koa/router"));
|
|
41
|
+
const roomsDb = __importStar(require("../../services/group-chat/rooms-db"));
|
|
42
|
+
exports.groupChatRoutes = new router_1.default();
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Rooms
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
exports.groupChatRoutes.get('/api/hermes/group-chat/rooms', async (ctx) => {
|
|
47
|
+
const rooms = roomsDb.listRooms();
|
|
48
|
+
ctx.body = { rooms };
|
|
49
|
+
});
|
|
50
|
+
exports.groupChatRoutes.post('/api/hermes/group-chat/rooms', async (ctx) => {
|
|
51
|
+
const { name } = ctx.request.body;
|
|
52
|
+
if (!name?.trim()) {
|
|
53
|
+
ctx.status = 400;
|
|
54
|
+
ctx.body = { error: 'name is required' };
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const room = roomsDb.createRoom(name.trim());
|
|
58
|
+
ctx.body = { room };
|
|
59
|
+
});
|
|
60
|
+
exports.groupChatRoutes.get('/api/hermes/group-chat/rooms/:roomId', async (ctx) => {
|
|
61
|
+
const { roomId } = ctx.params;
|
|
62
|
+
const room = roomsDb.getRoom(roomId);
|
|
63
|
+
if (!room) {
|
|
64
|
+
ctx.status = 404;
|
|
65
|
+
ctx.body = { error: 'Room not found' };
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const agents = roomsDb.getRoomAgents(roomId);
|
|
69
|
+
ctx.body = { room, agents };
|
|
70
|
+
});
|
|
71
|
+
exports.groupChatRoutes.put('/api/hermes/group-chat/rooms/:roomId', async (ctx) => {
|
|
72
|
+
const { roomId } = ctx.params;
|
|
73
|
+
const { name, settings } = ctx.request.body;
|
|
74
|
+
const updated = roomsDb.updateRoom(roomId, { name, settings });
|
|
75
|
+
if (!updated) {
|
|
76
|
+
ctx.status = 404;
|
|
77
|
+
ctx.body = { error: 'Room not found' };
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
ctx.body = { success: true };
|
|
81
|
+
});
|
|
82
|
+
exports.groupChatRoutes.delete('/api/hermes/group-chat/rooms/:roomId', async (ctx) => {
|
|
83
|
+
const { roomId } = ctx.params;
|
|
84
|
+
roomsDb.deleteRoom(roomId);
|
|
85
|
+
ctx.body = { success: true };
|
|
86
|
+
});
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Agents
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
exports.groupChatRoutes.get('/api/hermes/group-chat/rooms/:roomId/agents', async (ctx) => {
|
|
91
|
+
const { roomId } = ctx.params;
|
|
92
|
+
const agents = roomsDb.getRoomAgents(roomId);
|
|
93
|
+
ctx.body = { agents };
|
|
94
|
+
});
|
|
95
|
+
exports.groupChatRoutes.post('/api/hermes/group-chat/rooms/:roomId/agents', async (ctx) => {
|
|
96
|
+
const { roomId } = ctx.params;
|
|
97
|
+
const { agent_name, profile, role_prompt } = ctx.request.body;
|
|
98
|
+
if (!agent_name?.trim() || !profile?.trim()) {
|
|
99
|
+
ctx.status = 400;
|
|
100
|
+
ctx.body = { error: 'agent_name and profile are required' };
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// Validate agent_name only contains word characters
|
|
104
|
+
if (!/^\w+$/.test(agent_name.trim())) {
|
|
105
|
+
ctx.status = 400;
|
|
106
|
+
ctx.body = { error: 'agent_name must only contain letters, numbers, and underscores' };
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const agent = roomsDb.addRoomAgent(roomId, {
|
|
110
|
+
agent_name: agent_name.trim(),
|
|
111
|
+
profile: profile.trim(),
|
|
112
|
+
role_prompt,
|
|
113
|
+
});
|
|
114
|
+
if (!agent) {
|
|
115
|
+
ctx.status = 409;
|
|
116
|
+
ctx.body = { error: 'Agent already exists in this room' };
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
ctx.body = { agent };
|
|
120
|
+
});
|
|
121
|
+
exports.groupChatRoutes.delete('/api/hermes/group-chat/rooms/:roomId/agents/:agentName', async (ctx) => {
|
|
122
|
+
const { roomId, agentName } = ctx.params;
|
|
123
|
+
roomsDb.removeRoomAgent(roomId, agentName);
|
|
124
|
+
ctx.body = { success: true };
|
|
125
|
+
});
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// Messages
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
exports.groupChatRoutes.get('/api/hermes/group-chat/rooms/:roomId/messages', async (ctx) => {
|
|
130
|
+
const { roomId } = ctx.params;
|
|
131
|
+
const limit = parseInt(ctx.query.limit) || 100;
|
|
132
|
+
const offset = parseInt(ctx.query.offset) || 0;
|
|
133
|
+
const messages = roomsDb.getRoomMessages(roomId, limit, offset);
|
|
134
|
+
ctx.body = { messages };
|
|
135
|
+
});
|
|
@@ -38,28 +38,43 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.logRoutes = void 0;
|
|
40
40
|
const router_1 = __importDefault(require("@koa/router"));
|
|
41
|
+
const fs_1 = require("fs");
|
|
42
|
+
const promises_1 = require("fs/promises");
|
|
43
|
+
const path_1 = require("path");
|
|
44
|
+
const os_1 = require("os");
|
|
41
45
|
const hermesCli = __importStar(require("../../services/hermes/hermes-cli"));
|
|
42
46
|
exports.logRoutes = new router_1.default();
|
|
47
|
+
const WEBUI_LOG_FILE = (0, path_1.join)((0, os_1.homedir)(), '.hermes-web-ui', 'server.log');
|
|
43
48
|
// List available log files
|
|
44
49
|
exports.logRoutes.get('/api/hermes/logs', async (ctx) => {
|
|
45
50
|
const files = await hermesCli.listLogFiles();
|
|
51
|
+
if ((0, fs_1.existsSync)(WEBUI_LOG_FILE)) {
|
|
52
|
+
try {
|
|
53
|
+
const stat = (0, fs_1.statSync)(WEBUI_LOG_FILE);
|
|
54
|
+
const size = stat.size > 1024 * 1024
|
|
55
|
+
? `${(stat.size / 1024 / 1024).toFixed(1)}MB`
|
|
56
|
+
: `${(stat.size / 1024).toFixed(1)}KB`;
|
|
57
|
+
const modified = stat.mtime.toLocaleString();
|
|
58
|
+
files.push({ name: 'webui', size, modified });
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
}
|
|
46
62
|
ctx.body = { files };
|
|
47
63
|
});
|
|
48
64
|
// Parse a single log line into structured entry
|
|
49
65
|
function parseLine(line) {
|
|
50
|
-
// Match: 2026-04-11 20:16:16,289 INFO aiohttp.access: message
|
|
51
|
-
|
|
66
|
+
// Match: 2026-04-11 20:16:16,289 INFO aiohttp.access: message (agent log format)
|
|
67
|
+
let match = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\s+(DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+(\S+?):\s(.*)$/);
|
|
52
68
|
if (match) {
|
|
53
|
-
return {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
};
|
|
69
|
+
return { timestamp: match[1], level: match[2], logger: match[3], message: match[4], raw: line };
|
|
70
|
+
}
|
|
71
|
+
// Match: [Lark] [2026-04-19 18:46:54,864] [INFO] message (gateway log format)
|
|
72
|
+
match = line.match(/^\[(\S+?)\]\s+\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\]\s+\[(DEBUG|INFO|WARNING|ERROR|CRITICAL)\]\s(.*)$/);
|
|
73
|
+
if (match) {
|
|
74
|
+
return { timestamp: match[2], level: match[3], logger: match[1], message: match[4], raw: line };
|
|
60
75
|
}
|
|
61
|
-
// Unparseable line
|
|
62
|
-
return
|
|
76
|
+
// Unparseable line — keep as raw entry so nothing is lost
|
|
77
|
+
return { timestamp: '', level: '', logger: '', message: line, raw: line };
|
|
63
78
|
}
|
|
64
79
|
// Read log lines (parsed)
|
|
65
80
|
exports.logRoutes.get('/api/hermes/logs/:name', async (ctx) => {
|
|
@@ -68,6 +83,30 @@ exports.logRoutes.get('/api/hermes/logs/:name', async (ctx) => {
|
|
|
68
83
|
const level = ctx.query.level || undefined;
|
|
69
84
|
const session = ctx.query.session || undefined;
|
|
70
85
|
const since = ctx.query.since || undefined;
|
|
86
|
+
// Handle hermes-web-ui's own server log
|
|
87
|
+
if (logName === 'webui') {
|
|
88
|
+
try {
|
|
89
|
+
if (!(0, fs_1.existsSync)(WEBUI_LOG_FILE)) {
|
|
90
|
+
ctx.body = { entries: [] };
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const content = await (0, promises_1.readFile)(WEBUI_LOG_FILE, 'utf-8');
|
|
94
|
+
const rawLines = content.split('\n');
|
|
95
|
+
const sliced = rawLines.length > lines ? rawLines.slice(-lines) : rawLines;
|
|
96
|
+
const entries = [];
|
|
97
|
+
for (const line of sliced) {
|
|
98
|
+
if (!line.trim())
|
|
99
|
+
continue;
|
|
100
|
+
entries.push(parseLine(line));
|
|
101
|
+
}
|
|
102
|
+
ctx.body = { entries };
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
ctx.status = 500;
|
|
106
|
+
ctx.body = { error: err.message };
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
71
110
|
try {
|
|
72
111
|
const content = await hermesCli.readLogs(logName, lines, level, session, since);
|
|
73
112
|
const rawLines = content.split('\n');
|
|
@@ -27,22 +27,24 @@ async function waitForGatewayReady(upstream, timeoutMs = 5000) {
|
|
|
27
27
|
}
|
|
28
28
|
return false;
|
|
29
29
|
}
|
|
30
|
+
/** Resolve profile name from request */
|
|
31
|
+
function resolveProfile(ctx) {
|
|
32
|
+
return ctx.get('x-hermes-profile') || ctx.query.profile || 'default';
|
|
33
|
+
}
|
|
30
34
|
/** Resolve upstream URL for a request based on profile header/query */
|
|
31
35
|
function resolveUpstream(ctx) {
|
|
32
36
|
const mgr = (0, gateways_1.getGatewayManager)();
|
|
33
37
|
if (mgr) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (profile) {
|
|
38
|
+
const profile = resolveProfile(ctx);
|
|
39
|
+
if (profile && profile !== 'default') {
|
|
37
40
|
return mgr.getUpstream(profile);
|
|
38
41
|
}
|
|
39
|
-
// Default to active profile's upstream
|
|
40
42
|
return mgr.getUpstream();
|
|
41
43
|
}
|
|
42
|
-
// Fallback: static upstream from config
|
|
43
44
|
return config_1.config.upstream.replace(/\/$/, '');
|
|
44
45
|
}
|
|
45
46
|
async function proxy(ctx) {
|
|
47
|
+
const profile = resolveProfile(ctx);
|
|
46
48
|
const upstream = resolveUpstream(ctx);
|
|
47
49
|
// Rewrite path for upstream gateway:
|
|
48
50
|
// /api/hermes/v1/* -> /v1/* (upstream uses /v1/ prefix)
|
|
@@ -58,7 +60,7 @@ async function proxy(ctx) {
|
|
|
58
60
|
if (lower === 'host') {
|
|
59
61
|
headers['host'] = new URL(upstream).host;
|
|
60
62
|
}
|
|
61
|
-
else if (lower === '
|
|
63
|
+
else if (lower === 'origin' || lower === 'referer' || lower === 'connection') {
|
|
62
64
|
continue;
|
|
63
65
|
}
|
|
64
66
|
else {
|
|
@@ -67,6 +69,14 @@ async function proxy(ctx) {
|
|
|
67
69
|
headers[key] = v;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
72
|
+
// Inject Hermes gateway API key from profile's .env
|
|
73
|
+
const mgr = (0, gateways_1.getGatewayManager)();
|
|
74
|
+
if (mgr) {
|
|
75
|
+
const apiKey = mgr.getApiKey(profile);
|
|
76
|
+
if (apiKey) {
|
|
77
|
+
headers['authorization'] = `Bearer ${apiKey}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
70
80
|
try {
|
|
71
81
|
// Build request body from raw body
|
|
72
82
|
let body;
|
|
@@ -23,28 +23,58 @@ exports.uploadRoutes.post('/upload', async (ctx) => {
|
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
await (0, promises_1.mkdir)(config_1.config.uploadDir, { recursive: true });
|
|
26
|
-
// Read raw body
|
|
26
|
+
// Read raw body as Buffer
|
|
27
27
|
const chunks = [];
|
|
28
28
|
for await (const chunk of ctx.req)
|
|
29
29
|
chunks.push(chunk);
|
|
30
|
-
const
|
|
31
|
-
const
|
|
30
|
+
const raw = Buffer.concat(chunks);
|
|
31
|
+
const boundaryBuf = Buffer.from(boundary);
|
|
32
|
+
const parts = splitMultipart(raw, boundaryBuf);
|
|
32
33
|
const results = [];
|
|
33
34
|
for (const part of parts) {
|
|
34
|
-
const headerEnd = part.indexOf('\r\n\r\n');
|
|
35
|
+
const headerEnd = part.indexOf(Buffer.from('\r\n\r\n'));
|
|
35
36
|
if (headerEnd === -1)
|
|
36
37
|
continue;
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
38
|
+
const headerBuf = part.subarray(0, headerEnd);
|
|
39
|
+
const header = headerBuf.toString('utf-8');
|
|
40
|
+
const data = part.subarray(headerEnd + 4, part.length - 2);
|
|
41
|
+
// Try RFC 5987 filename* first, fall back to filename
|
|
42
|
+
let filename = '';
|
|
43
|
+
const filenameStarMatch = header.match(/filename\*=UTF-8''(.+)/i);
|
|
44
|
+
if (filenameStarMatch) {
|
|
45
|
+
filename = decodeURIComponent(filenameStarMatch[1]);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const filenameMatch = header.match(/filename="([^"]+)"/);
|
|
49
|
+
if (!filenameMatch)
|
|
50
|
+
continue;
|
|
51
|
+
filename = filenameMatch[1];
|
|
52
|
+
}
|
|
43
53
|
const ext = filename.includes('.') ? '.' + filename.split('.').pop() : '';
|
|
44
54
|
const savedName = (0, crypto_1.randomBytes)(8).toString('hex') + ext;
|
|
45
55
|
const savedPath = `${config_1.config.uploadDir}/${savedName}`;
|
|
46
|
-
await (0, promises_1.writeFile)(savedPath,
|
|
56
|
+
await (0, promises_1.writeFile)(savedPath, data);
|
|
47
57
|
results.push({ name: filename, path: savedPath });
|
|
48
58
|
}
|
|
49
59
|
ctx.body = { files: results };
|
|
50
60
|
});
|
|
61
|
+
/**
|
|
62
|
+
* Split a multipart Buffer by boundary, returning part Buffers.
|
|
63
|
+
* Avoids string decoding so multi-byte characters (e.g. Chinese filenames) are preserved.
|
|
64
|
+
*/
|
|
65
|
+
function splitMultipart(raw, boundary) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
let start = 0;
|
|
68
|
+
while (true) {
|
|
69
|
+
const idx = raw.indexOf(boundary, start);
|
|
70
|
+
if (idx === -1)
|
|
71
|
+
break;
|
|
72
|
+
if (start > 0) {
|
|
73
|
+
// Skip the \r\n after boundary
|
|
74
|
+
const partStart = start + 2;
|
|
75
|
+
parts.push(raw.subarray(partStart, idx));
|
|
76
|
+
}
|
|
77
|
+
start = idx + boundary.length;
|
|
78
|
+
}
|
|
79
|
+
return parts;
|
|
80
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as roomsDb from './rooms-db';
|
|
2
|
+
export type AgentStatus = 'thinking' | 'responding' | 'done' | 'error';
|
|
3
|
+
export interface CoordinatorCallbacks {
|
|
4
|
+
onDelta: (roomId: string, agentName: string, text: string) => void;
|
|
5
|
+
onAgentStatus: (roomId: string, agentName: string, status: AgentStatus) => void;
|
|
6
|
+
onAgentComplete: (roomId: string, agentName: string, message: roomsDb.RoomMessage) => void;
|
|
7
|
+
onAgentError: (roomId: string, agentName: string, error: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseMentions(text: string): string[];
|
|
10
|
+
export declare function buildAgentContext(roomId: string, agentName: string, agents: roomsDb.RoomAgent[], maxMessages?: number): Array<{
|
|
11
|
+
role: string;
|
|
12
|
+
content: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function processMessage(roomId: string, content: string, callbacks: CoordinatorCallbacks): Promise<void>;
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.parseMentions = parseMentions;
|
|
37
|
+
exports.buildAgentContext = buildAgentContext;
|
|
38
|
+
exports.processMessage = processMessage;
|
|
39
|
+
const roomsDb = __importStar(require("./rooms-db"));
|
|
40
|
+
const gateways_1 = require("../../routes/hermes/gateways");
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// @mention parsing
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
function parseMentions(text) {
|
|
45
|
+
const pattern = /@(\w+)/g;
|
|
46
|
+
const mentions = [];
|
|
47
|
+
let match;
|
|
48
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
49
|
+
mentions.push(match[1]);
|
|
50
|
+
}
|
|
51
|
+
return [...new Set(mentions)];
|
|
52
|
+
}
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// Context building
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function buildAgentContext(roomId, agentName, agents, maxMessages = 20) {
|
|
57
|
+
const recentMessages = roomsDb.getRecentContext(roomId, agentName, maxMessages);
|
|
58
|
+
const context = [];
|
|
59
|
+
// Add role prompt
|
|
60
|
+
const agent = agents.find((a) => a.agent_name === agentName);
|
|
61
|
+
if (agent?.role_prompt) {
|
|
62
|
+
context.push({ role: 'system', content: agent.role_prompt });
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
context.push({
|
|
66
|
+
role: 'system',
|
|
67
|
+
content: `You are ${agentName}, participating in a group chat. Respond concisely. You can see messages where you were @mentioned and public messages (no @mentions).`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Convert recent messages (DESC from DB → reverse to ASC chronological)
|
|
71
|
+
for (const msg of recentMessages.reverse()) {
|
|
72
|
+
if (msg.role === 'user') {
|
|
73
|
+
context.push({ role: 'user', content: msg.content });
|
|
74
|
+
}
|
|
75
|
+
else if (msg.role === 'assistant' && msg.agent_name) {
|
|
76
|
+
context.push({ role: 'assistant', content: `[${msg.agent_name}] ${msg.content}` });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return context;
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Main: process a user message
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
async function processMessage(roomId, content, callbacks) {
|
|
85
|
+
const mentions = parseMentions(content);
|
|
86
|
+
const agents = roomsDb.getRoomAgents(roomId);
|
|
87
|
+
if (agents.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
// Determine responding agents
|
|
90
|
+
const respondingAgents = mentions.length > 0
|
|
91
|
+
? agents.filter((a) => mentions.includes(a.agent_name))
|
|
92
|
+
: agents;
|
|
93
|
+
if (respondingAgents.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
// Store user message
|
|
96
|
+
const round = roomsDb.getLatestRound(roomId);
|
|
97
|
+
roomsDb.addMessage({
|
|
98
|
+
room_id: roomId,
|
|
99
|
+
role: 'user',
|
|
100
|
+
content,
|
|
101
|
+
mentions,
|
|
102
|
+
round,
|
|
103
|
+
});
|
|
104
|
+
// Fan out to agents in parallel
|
|
105
|
+
const tasks = respondingAgents.map((agent) => invokeAgent(roomId, agent, round, content, callbacks).catch(() => { }));
|
|
106
|
+
await Promise.allSettled(tasks);
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// Invoke a single agent
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
async function invokeAgent(roomId, agent, round, userMessage, callbacks) {
|
|
112
|
+
const agentName = agent.agent_name;
|
|
113
|
+
let upstream = '';
|
|
114
|
+
let apiKey = null;
|
|
115
|
+
try {
|
|
116
|
+
callbacks.onAgentStatus(roomId, agentName, 'thinking');
|
|
117
|
+
// Resolve gateway URL and API key for this agent's profile
|
|
118
|
+
const manager = (0, gateways_1.getGatewayManager)();
|
|
119
|
+
if (!manager)
|
|
120
|
+
throw new Error('GatewayManager not initialized');
|
|
121
|
+
upstream = manager.getUpstream(agent.profile);
|
|
122
|
+
apiKey = manager.getApiKey(agent.profile);
|
|
123
|
+
// Build conversation context
|
|
124
|
+
const agents = roomsDb.getRoomAgents(roomId);
|
|
125
|
+
const context = buildAgentContext(roomId, agentName, agents);
|
|
126
|
+
// Start run via Hermes API Server
|
|
127
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
128
|
+
if (apiKey)
|
|
129
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
130
|
+
const body = { input: userMessage };
|
|
131
|
+
if (context.length > 0)
|
|
132
|
+
body.conversation_history = context;
|
|
133
|
+
if (agent.session_id)
|
|
134
|
+
body.session_id = agent.session_id;
|
|
135
|
+
const runRes = await fetch(`${upstream}/v1/runs`, {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
headers,
|
|
138
|
+
body: JSON.stringify(body),
|
|
139
|
+
signal: AbortSignal.timeout(120_000),
|
|
140
|
+
});
|
|
141
|
+
if (!runRes.ok) {
|
|
142
|
+
const errText = await runRes.text();
|
|
143
|
+
throw new Error(`Hermes API error ${runRes.status}: ${errText.slice(0, 200)}`);
|
|
144
|
+
}
|
|
145
|
+
const runData = await runRes.json();
|
|
146
|
+
const runId = runData.run_id || runData.id;
|
|
147
|
+
if (!runId)
|
|
148
|
+
throw new Error('No run_id returned from Hermes API');
|
|
149
|
+
// Persist session_id for subsequent messages
|
|
150
|
+
if (runData.session_id && runData.session_id !== agent.session_id) {
|
|
151
|
+
roomsDb.updateRoomAgentSession(roomId, agentName, runData.session_id);
|
|
152
|
+
}
|
|
153
|
+
callbacks.onAgentStatus(roomId, agentName, 'responding');
|
|
154
|
+
// Stream SSE events
|
|
155
|
+
const sseHeaders = {};
|
|
156
|
+
if (apiKey)
|
|
157
|
+
sseHeaders['Authorization'] = `Bearer ${apiKey}`;
|
|
158
|
+
const sseRes = await fetch(`${upstream}/v1/runs/${runId}/events`, {
|
|
159
|
+
headers: sseHeaders,
|
|
160
|
+
signal: AbortSignal.timeout(300_000),
|
|
161
|
+
});
|
|
162
|
+
if (!sseRes.ok || !sseRes.body) {
|
|
163
|
+
throw new Error(`Failed to stream events: ${sseRes.status}`);
|
|
164
|
+
}
|
|
165
|
+
let fullContent = '';
|
|
166
|
+
const reader = sseRes.body.getReader();
|
|
167
|
+
const decoder = new TextDecoder();
|
|
168
|
+
let buffer = '';
|
|
169
|
+
try {
|
|
170
|
+
while (true) {
|
|
171
|
+
const { done, value } = await reader.read();
|
|
172
|
+
if (done)
|
|
173
|
+
break;
|
|
174
|
+
buffer += decoder.decode(value, { stream: true });
|
|
175
|
+
const lines = buffer.split('\n');
|
|
176
|
+
buffer = lines.pop() || '';
|
|
177
|
+
for (const line of lines) {
|
|
178
|
+
if (!line.startsWith('data: '))
|
|
179
|
+
continue;
|
|
180
|
+
const data = line.slice(6).trim();
|
|
181
|
+
if (data === '[DONE]')
|
|
182
|
+
continue;
|
|
183
|
+
try {
|
|
184
|
+
const event = JSON.parse(data);
|
|
185
|
+
if (event.event === 'message.delta' && event.delta) {
|
|
186
|
+
fullContent += event.delta;
|
|
187
|
+
callbacks.onDelta(roomId, agentName, event.delta);
|
|
188
|
+
}
|
|
189
|
+
if (event.event === 'run.completed' || event.event === 'run.failed') {
|
|
190
|
+
// If delta contained in completed event
|
|
191
|
+
if (event.event === 'run.completed' && event.response) {
|
|
192
|
+
fullContent = event.response;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// skip malformed JSON
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
reader.releaseLock();
|
|
204
|
+
}
|
|
205
|
+
if (!fullContent)
|
|
206
|
+
fullContent = '(no response)';
|
|
207
|
+
// Store completed response
|
|
208
|
+
const msg = roomsDb.addMessage({
|
|
209
|
+
room_id: roomId,
|
|
210
|
+
role: 'assistant',
|
|
211
|
+
agent_name: agentName,
|
|
212
|
+
content: fullContent,
|
|
213
|
+
round,
|
|
214
|
+
});
|
|
215
|
+
callbacks.onAgentStatus(roomId, agentName, 'done');
|
|
216
|
+
callbacks.onAgentComplete(roomId, agentName, msg);
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
const errorMsg = err?.message || 'Unknown error';
|
|
220
|
+
callbacks.onAgentStatus(roomId, agentName, 'error');
|
|
221
|
+
callbacks.onAgentError(roomId, agentName, errorMsg);
|
|
222
|
+
roomsDb.addMessage({
|
|
223
|
+
room_id: roomId,
|
|
224
|
+
role: 'system',
|
|
225
|
+
agent_name: agentName,
|
|
226
|
+
content: `Error: ${errorMsg}`,
|
|
227
|
+
round,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Server as SocketIOServer } from 'socket.io';
|
|
2
|
+
import type { Server as HttpServer } from 'http';
|
|
3
|
+
export declare function setupGroupChatSocketIO(httpServer: HttpServer): SocketIOServer;
|
|
4
|
+
export declare function getGroupChatIO(): SocketIOServer | null;
|
|
5
|
+
export declare function disconnectGroupChatIO(): void;
|