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.
Files changed (87) hide show
  1. package/README.md +21 -2
  2. package/bin/hermes-web-ui.mjs +15 -1
  3. package/dist/client/assets/{Add-CKf6ViXR.js → Add-s316k4Av.js} +1 -1
  4. package/dist/client/assets/{Button-CrrCCorI.js → Button-BkA_RI8a.js} +1 -1
  5. package/dist/client/assets/{ChannelsView-D4I7hhZO.js → ChannelsView-DPmPn8DW.js} +1 -1
  6. package/dist/client/assets/ChatView-DEtziOPB.js +127 -0
  7. package/dist/client/assets/{ChatView-Vfi_jEpI.css → ChatView-DI3XN8vz.css} +1 -1
  8. package/dist/client/assets/{Close-C9xwy-pW.js → Close-CrLD0IXG.js} +1 -1
  9. package/dist/client/assets/{FormItem-BgJdrTW0.js → FormItem-CQLdFrl9.js} +1 -1
  10. package/dist/client/assets/{GatewaysView-Cib2JydO.js → GatewaysView-CC1Y0tZZ.js} +1 -1
  11. package/dist/client/assets/{Input-ChENEW-Z.js → Input-nXKlujwJ.js} +1 -1
  12. package/dist/client/assets/{InputNumber-Xd6HWSdp.js → InputNumber-DLZwwIyX.js} +1 -1
  13. package/dist/client/assets/{JobsView-SnToCbDd.js → JobsView-BC0bBrJO.js} +2 -2
  14. package/dist/client/assets/{LoginView-BZdmMnsf.js → LoginView-PqpFR9bV.js} +1 -1
  15. package/dist/client/assets/{LogsView-DblvOJIg.js → LogsView-DtR88N0b.js} +1 -1
  16. package/dist/client/assets/{MarkdownRenderer-DJLVk7ei.js → MarkdownRenderer-BSLfTurm.js} +1 -1
  17. package/dist/client/assets/{MemoryView-exXvRwCc.js → MemoryView-CJRWnePL.js} +1 -1
  18. package/dist/client/assets/{Modal-B2zvXTrk.js → Modal-Bp9RK8LZ.js} +1 -1
  19. package/dist/client/assets/ModelsView-7Obe34Cz.js +1 -0
  20. package/dist/client/assets/{ModelsView-jbgZP3YF.css → ModelsView-VM-oMq5M.css} +1 -1
  21. package/dist/client/assets/{Popconfirm-BoZc0kKk.js → Popconfirm-DTdUi7r_.js} +1 -1
  22. package/dist/client/assets/{Popover-Cu52vG3D.js → Popover-_M3o0B7L.js} +1 -1
  23. package/dist/client/assets/{ProfilesView-D0FY7Jwe.js → ProfilesView-1_GmRx-S.js} +1 -1
  24. package/dist/client/assets/{Select-BHc7u-Yf.js → Select-aHPR3urY.js} +2 -2
  25. package/dist/client/assets/{SettingRow-i-UXlco7.js → SettingRow-DKasLuS5.js} +1 -1
  26. package/dist/client/assets/{SettingsView-Dhr2wzAB.css → SettingsView-C3sd8K0e.css} +1 -1
  27. package/dist/client/assets/SettingsView-DZCA7_CM.js +352 -0
  28. package/dist/client/assets/{SkillsView-B5QBaAzi.js → SkillsView-Dk7O05cK.js} +1 -1
  29. package/dist/client/assets/{Spin-DsNCRPk9.js → Spin-Bt_9cTiO.js} +1 -1
  30. package/dist/client/assets/{Suffix-3xK0KZGt.js → Suffix-XaH8SDbR.js} +1 -1
  31. package/dist/client/assets/{Switch-Bf63XXgA.js → Switch-D1_psmjT.js} +1 -1
  32. package/dist/client/assets/{Tag-Dmbj68Ki.js → Tag-3FaOhoJN.js} +1 -1
  33. package/dist/client/assets/{TerminalView-DrJHZ0qI.js → TerminalView-DNU7oQxK.js} +1 -1
  34. package/dist/client/assets/{Tooltip-CRbZNhG0.js → Tooltip-YHrHWGPa.js} +1 -1
  35. package/dist/client/assets/{UsageView-DQ43JasX.js → UsageView-COCrOiiV.js} +1 -1
  36. package/dist/client/assets/{Warning-kBbRMAif.js → Warning-B6CM9aBl.js} +1 -1
  37. package/dist/client/assets/{_plugin-vue_export-helper-CnosYBkx.js → _plugin-vue_export-helper-BGG8ORDx.js} +1 -1
  38. package/dist/client/assets/app-B7ktf7Fh.js +1 -0
  39. package/dist/client/assets/app-BPvTl2-V.js +1 -0
  40. package/dist/client/assets/{browser-Djp4tkp3.js → browser-f5W8abIG.js} +1 -1
  41. package/dist/client/assets/chat-6q6pkzEW.js +6 -0
  42. package/dist/client/assets/composables-xV7dhNpf.js +1 -0
  43. package/dist/client/assets/{fade-in.cssr-CIVyTG6A.js → fade-in.cssr-ifHK7yH1.js} +1 -1
  44. package/dist/client/assets/index-CSCYx7ux.js +284 -0
  45. package/dist/client/assets/{jobs-CcVaCGMJ.js → jobs-DObWfhbO.js} +1 -1
  46. package/dist/client/assets/{light-D9G2GshF.js → light-CxjyoF0s.js} +1 -1
  47. package/dist/client/assets/{light-KCEDTUGE.js → light-D1yfed_s.js} +1 -1
  48. package/dist/client/assets/{light-BPqyaxve.js → light-DWy-mwyK.js} +1 -1
  49. package/dist/client/assets/{light-CSp9-LhE.js → light-D_3MwJj1.js} +1 -1
  50. package/dist/client/assets/{light-BF6E9z0k.js → light-DgLcPjgU.js} +1 -1
  51. package/dist/client/assets/{light-BJ96fCLC.js → light-TGFKT-UB.js} +1 -1
  52. package/dist/client/assets/models-DQ4CT-vv.js +1 -0
  53. package/dist/client/assets/{pinia-iHE5_ZXa.js → pinia-DcAkZ8vx.js} +1 -1
  54. package/dist/client/assets/{profiles-CJCR84uQ.js → profiles-DzkigJwq.js} +1 -1
  55. package/dist/client/assets/{router-C-NNJUuf.js → router-D8sJ39Io.js} +2 -2
  56. package/dist/client/assets/{sessions-C4bnNvzS.js → sessions-Dg8n9PBo.js} +1 -1
  57. package/dist/client/assets/{skills-B4slZfeZ.js → skills-BehzdECn.js} +1 -1
  58. package/dist/client/assets/{use-message-BIpqgDet.js → use-message-DBz2JSTt.js} +1 -1
  59. package/dist/client/assets/{useTheme-B78N9tyz.js → useTheme-UdVT814n.js} +1 -1
  60. package/dist/client/index.html +27 -27
  61. package/dist/server/index.js +8 -3
  62. package/dist/server/routes/hermes/filesystem.js +231 -199
  63. package/dist/server/routes/hermes/group-chat.d.ts +2 -0
  64. package/dist/server/routes/hermes/group-chat.js +135 -0
  65. package/dist/server/routes/hermes/logs.js +50 -11
  66. package/dist/server/routes/hermes/proxy-handler.js +16 -6
  67. package/dist/server/routes/upload.js +41 -11
  68. package/dist/server/services/group-chat/coordinator.d.ts +14 -0
  69. package/dist/server/services/group-chat/coordinator.js +230 -0
  70. package/dist/server/services/group-chat/index.d.ts +5 -0
  71. package/dist/server/services/group-chat/index.js +115 -0
  72. package/dist/server/services/group-chat/rooms-db.d.ts +56 -0
  73. package/dist/server/services/group-chat/rooms-db.js +199 -0
  74. package/dist/server/services/hermes/gateway-manager.d.ts +2 -0
  75. package/dist/server/services/hermes/gateway-manager.js +15 -0
  76. package/dist/server/services/hermes/hermes-profile.d.ts +6 -0
  77. package/dist/server/services/hermes/hermes-profile.js +12 -0
  78. package/dist/server/shared/providers.js +1 -13
  79. package/package.json +1 -1
  80. package/dist/client/assets/ChatView-DxyBUK57.js +0 -127
  81. package/dist/client/assets/ModelsView-DGs47Cj4.js +0 -1
  82. package/dist/client/assets/SettingsView-BW6ctYG5.js +0 -352
  83. package/dist/client/assets/app-BT9yU6N6.js +0 -1
  84. package/dist/client/assets/app-CjNVVG5x.js +0 -1
  85. package/dist/client/assets/chat-DlC9S9DK.js +0 -6
  86. package/dist/client/assets/composables-DCA4Yga5.js +0 -1
  87. 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
- const 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(.*)$/);
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
- timestamp: match[1],
55
- level: match[2],
56
- logger: match[3],
57
- message: match[4],
58
- raw: line,
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 (e.g. traceback continuation)
62
- return null;
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
- // Check X-Hermes-Profile header or ?profile= query param
35
- const profile = ctx.get('x-hermes-profile') || ctx.query.profile;
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 === 'authorization' || lower === 'origin' || lower === 'referer' || lower === 'connection') {
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 body = Buffer.concat(chunks).toString('latin1');
31
- const parts = body.split(boundary).slice(1, -1);
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 header = part.substring(0, headerEnd);
38
- const data = part.substring(headerEnd + 4, part.length - 2);
39
- const filenameMatch = header.match(/filename="([^"]+)"/);
40
- if (!filenameMatch)
41
- continue;
42
- const filename = filenameMatch[1];
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, Buffer.from(data, 'binary'));
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;