@0xmaxma/claude-gateway 1.0.0
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 +791 -0
- package/config.template.json +87 -0
- package/dist/agent/context-isolation.d.ts +28 -0
- package/dist/agent/context-isolation.d.ts.map +1 -0
- package/dist/agent/context-isolation.js +80 -0
- package/dist/agent/context-isolation.js.map +1 -0
- package/dist/agent/runner.d.ts +211 -0
- package/dist/agent/runner.d.ts.map +1 -0
- package/dist/agent/runner.js +1802 -0
- package/dist/agent/runner.js.map +1 -0
- package/dist/agent/workspace-loader.d.ts +31 -0
- package/dist/agent/workspace-loader.d.ts.map +1 -0
- package/dist/agent/workspace-loader.js +207 -0
- package/dist/agent/workspace-loader.js.map +1 -0
- package/dist/api/auth.d.ts +25 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +77 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/cron-router.d.ts +22 -0
- package/dist/api/cron-router.d.ts.map +1 -0
- package/dist/api/cron-router.js +197 -0
- package/dist/api/cron-router.js.map +1 -0
- package/dist/api/gateway-router.d.ts +52 -0
- package/dist/api/gateway-router.d.ts.map +1 -0
- package/dist/api/gateway-router.js +215 -0
- package/dist/api/gateway-router.js.map +1 -0
- package/dist/api/router.d.ts +5 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +1098 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/skills-router.d.ts +5 -0
- package/dist/api/skills-router.d.ts.map +1 -0
- package/dist/api/skills-router.js +424 -0
- package/dist/api/skills-router.js.map +1 -0
- package/dist/api/workspace-router.d.ts +4 -0
- package/dist/api/workspace-router.d.ts.map +1 -0
- package/dist/api/workspace-router.js +149 -0
- package/dist/api/workspace-router.js.map +1 -0
- package/dist/config/loader.d.ts +16 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +235 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/migrator.d.ts +70 -0
- package/dist/config/migrator.d.ts.map +1 -0
- package/dist/config/migrator.js +343 -0
- package/dist/config/migrator.js.map +1 -0
- package/dist/config/watcher.d.ts +30 -0
- package/dist/config/watcher.d.ts.map +1 -0
- package/dist/config/watcher.js +170 -0
- package/dist/config/watcher.js.map +1 -0
- package/dist/cron/manager.d.ts +48 -0
- package/dist/cron/manager.d.ts.map +1 -0
- package/dist/cron/manager.js +580 -0
- package/dist/cron/manager.js.map +1 -0
- package/dist/cron/scheduler.d.ts +50 -0
- package/dist/cron/scheduler.d.ts.map +1 -0
- package/dist/cron/scheduler.js +231 -0
- package/dist/cron/scheduler.js.map +1 -0
- package/dist/discord/receiver.d.ts +17 -0
- package/dist/discord/receiver.d.ts.map +1 -0
- package/dist/discord/receiver.js +108 -0
- package/dist/discord/receiver.js.map +1 -0
- package/dist/heartbeat/history.d.ts +24 -0
- package/dist/heartbeat/history.d.ts.map +1 -0
- package/dist/heartbeat/history.js +50 -0
- package/dist/heartbeat/history.js.map +1 -0
- package/dist/heartbeat/parser.d.ts +26 -0
- package/dist/heartbeat/parser.d.ts.map +1 -0
- package/dist/heartbeat/parser.js +283 -0
- package/dist/heartbeat/parser.js.map +1 -0
- package/dist/history/cleanup.d.ts +21 -0
- package/dist/history/cleanup.d.ts.map +1 -0
- package/dist/history/cleanup.js +171 -0
- package/dist/history/cleanup.js.map +1 -0
- package/dist/history/db.d.ts +27 -0
- package/dist/history/db.d.ts.map +1 -0
- package/dist/history/db.js +323 -0
- package/dist/history/db.js.map +1 -0
- package/dist/history/media-store.d.ts +27 -0
- package/dist/history/media-store.d.ts.map +1 -0
- package/dist/history/media-store.js +158 -0
- package/dist/history/media-store.js.map +1 -0
- package/dist/history/types.d.ts +62 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/history/types.js +3 -0
- package/dist/history/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +483 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +3 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +81 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory/manager.d.ts +45 -0
- package/dist/memory/manager.d.ts.map +1 -0
- package/dist/memory/manager.js +178 -0
- package/dist/memory/manager.js.map +1 -0
- package/dist/session/compactor.d.ts +22 -0
- package/dist/session/compactor.d.ts.map +1 -0
- package/dist/session/compactor.js +151 -0
- package/dist/session/compactor.js.map +1 -0
- package/dist/session/process.d.ts +76 -0
- package/dist/session/process.d.ts.map +1 -0
- package/dist/session/process.js +729 -0
- package/dist/session/process.js.map +1 -0
- package/dist/session/store.d.ts +102 -0
- package/dist/session/store.d.ts.map +1 -0
- package/dist/session/store.js +533 -0
- package/dist/session/store.js.map +1 -0
- package/dist/skills/index.d.ts +9 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +15 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/invoker.d.ts +16 -0
- package/dist/skills/invoker.d.ts.map +1 -0
- package/dist/skills/invoker.js +36 -0
- package/dist/skills/invoker.js.map +1 -0
- package/dist/skills/loader.d.ts +23 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +183 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/parser.d.ts +55 -0
- package/dist/skills/parser.d.ts.map +1 -0
- package/dist/skills/parser.js +178 -0
- package/dist/skills/parser.js.map +1 -0
- package/dist/skills/sync.d.ts +10 -0
- package/dist/skills/sync.d.ts.map +1 -0
- package/dist/skills/sync.js +130 -0
- package/dist/skills/sync.js.map +1 -0
- package/dist/skills/watcher.d.ts +16 -0
- package/dist/skills/watcher.d.ts.map +1 -0
- package/dist/skills/watcher.js +25 -0
- package/dist/skills/watcher.js.map +1 -0
- package/dist/telegram/markdown.d.ts +25 -0
- package/dist/telegram/markdown.d.ts.map +1 -0
- package/dist/telegram/markdown.js +237 -0
- package/dist/telegram/markdown.js.map +1 -0
- package/dist/telegram/receiver.d.ts +17 -0
- package/dist/telegram/receiver.d.ts.map +1 -0
- package/dist/telegram/receiver.js +104 -0
- package/dist/telegram/receiver.js.map +1 -0
- package/dist/types.d.ts +224 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/web-ui.d.ts +6 -0
- package/dist/ui/web-ui.d.ts.map +1 -0
- package/dist/ui/web-ui.js +227 -0
- package/dist/ui/web-ui.js.map +1 -0
- package/dist/watch/factory.d.ts +26 -0
- package/dist/watch/factory.d.ts.map +1 -0
- package/dist/watch/factory.js +43 -0
- package/dist/watch/factory.js.map +1 -0
- package/dist/webhook/manager.d.ts +34 -0
- package/dist/webhook/manager.d.ts.map +1 -0
- package/dist/webhook/manager.js +120 -0
- package/dist/webhook/manager.js.map +1 -0
- package/mcp/bun.lock +264 -0
- package/mcp/channel-manager.ts +115 -0
- package/mcp/package.json +15 -0
- package/mcp/router.ts +69 -0
- package/mcp/server.ts +129 -0
- package/mcp/tools/agent/handlers.ts +578 -0
- package/mcp/tools/agent/module.ts +163 -0
- package/mcp/tools/browser/module.ts +321 -0
- package/mcp/tools/browser/skills/open-browser/SKILL.md +89 -0
- package/mcp/tools/cron/client.ts +65 -0
- package/mcp/tools/cron/module.ts +158 -0
- package/mcp/tools/cron/skills/cron/SKILL.md +46 -0
- package/mcp/tools/discord/access.ts +197 -0
- package/mcp/tools/discord/client.ts +31 -0
- package/mcp/tools/discord/commands.ts +48 -0
- package/mcp/tools/discord/inbound.ts +53 -0
- package/mcp/tools/discord/module.ts +442 -0
- package/mcp/tools/discord/outbound.ts +90 -0
- package/mcp/tools/discord/receiver-server.ts +58 -0
- package/mcp/tools/discord/skills/access/SKILL.md +86 -0
- package/mcp/tools/discord/skills/configure/SKILL.md +53 -0
- package/mcp/tools/discord/threading.ts +37 -0
- package/mcp/tools/discord/types.ts +106 -0
- package/mcp/tools/skills/handlers.ts +196 -0
- package/mcp/tools/skills/module.ts +163 -0
- package/mcp/tools/telegram/module.ts +454 -0
- package/mcp/tools/telegram/pure.ts +224 -0
- package/mcp/tools/telegram/receiver-server.ts +1656 -0
- package/mcp/tools/telegram/skills/access/SKILL.md +169 -0
- package/mcp/tools/telegram/skills/configure/SKILL.md +118 -0
- package/mcp/tools/telegram/typing.ts +327 -0
- package/mcp/types.ts +128 -0
- package/package.json +62 -0
- package/resource/claude_gateway.svg +112 -0
|
@@ -0,0 +1,1098 @@
|
|
|
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.createApiRouter = createApiRouter;
|
|
37
|
+
const express_1 = require("express");
|
|
38
|
+
const crypto_1 = require("crypto");
|
|
39
|
+
const fs = __importStar(require("fs"));
|
|
40
|
+
const fsp = __importStar(require("fs/promises"));
|
|
41
|
+
const os = __importStar(require("os"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const auth_1 = require("./auth");
|
|
44
|
+
const media_store_1 = require("../history/media-store");
|
|
45
|
+
const db_1 = require("../history/db");
|
|
46
|
+
const MAX_MESSAGE_LENGTH = 10000;
|
|
47
|
+
const DEFAULT_TIMEOUT_MS = 60000;
|
|
48
|
+
const AGENT_ID_RE = /^[a-z][a-z0-9_-]{1,31}$/;
|
|
49
|
+
const SAFE_FILENAME_RE = /^[a-zA-Z0-9._\-() ]+$/;
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// In-memory rate limiter for media uploads (per API key)
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
const UPLOAD_RATE_LIMIT = 20; // max uploads
|
|
54
|
+
const UPLOAD_RATE_WINDOW_MS = 60000; // per 60 seconds
|
|
55
|
+
const uploadRateMap = new Map();
|
|
56
|
+
function checkUploadRateLimit(apiKeyValue) {
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
const entry = uploadRateMap.get(apiKeyValue);
|
|
59
|
+
if (!entry || now >= entry.resetAt) {
|
|
60
|
+
uploadRateMap.set(apiKeyValue, { count: 1, resetAt: now + UPLOAD_RATE_WINDOW_MS });
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (entry.count >= UPLOAD_RATE_LIMIT)
|
|
64
|
+
return false;
|
|
65
|
+
entry.count++;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
// Periodically evict expired entries so uploadRateMap doesn't grow indefinitely
|
|
69
|
+
setInterval(() => {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
for (const [key, entry] of uploadRateMap) {
|
|
72
|
+
if (now >= entry.resetAt)
|
|
73
|
+
uploadRateMap.delete(key);
|
|
74
|
+
}
|
|
75
|
+
}, UPLOAD_RATE_WINDOW_MS).unref();
|
|
76
|
+
/** Derive a stable short ID from an API key value (never log the raw key). */
|
|
77
|
+
function apiKeyId(key) {
|
|
78
|
+
return (0, crypto_1.createHash)('sha256').update(key).digest('hex').slice(0, 16);
|
|
79
|
+
}
|
|
80
|
+
function createApiRouter(agentRunners, agentConfigs, apiKeys, configPath, models) {
|
|
81
|
+
const router = (0, express_1.Router)();
|
|
82
|
+
const auth = (0, auth_1.createApiAuthMiddleware)(apiKeys);
|
|
83
|
+
// Closure-scoped lock: serialises concurrent writes to config.json within this router instance.
|
|
84
|
+
let configWriteLock = Promise.resolve();
|
|
85
|
+
async function writeAgentsToConfigImpl(cfgPath, mutate, newId) {
|
|
86
|
+
const raw = await fsp.readFile(cfgPath, 'utf-8');
|
|
87
|
+
const config = JSON.parse(raw);
|
|
88
|
+
if (newId) {
|
|
89
|
+
const exists = config.agents.some((a) => a.id === newId);
|
|
90
|
+
if (exists)
|
|
91
|
+
throw Object.assign(new Error(`Agent '${newId}' already exists in config`), { code: 'DUPLICATE' });
|
|
92
|
+
}
|
|
93
|
+
mutate(config.agents);
|
|
94
|
+
const tmp = cfgPath + '.tmp.' + (0, crypto_1.randomUUID)();
|
|
95
|
+
await fsp.writeFile(tmp, JSON.stringify(config, null, 2), 'utf-8');
|
|
96
|
+
await fsp.rename(tmp, cfgPath);
|
|
97
|
+
}
|
|
98
|
+
function writeAgentsToConfig(cfgPath, mutate, newId) {
|
|
99
|
+
const next = configWriteLock.catch(() => { }).then(() => writeAgentsToConfigImpl(cfgPath, mutate, newId));
|
|
100
|
+
configWriteLock = next.catch(() => { });
|
|
101
|
+
return next;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* GET /api/v1/commands
|
|
105
|
+
*
|
|
106
|
+
* Return the list of slash commands available in the chat UI. No auth required.
|
|
107
|
+
*/
|
|
108
|
+
router.get('/v1/commands', (_req, res) => {
|
|
109
|
+
res.json({
|
|
110
|
+
commands: [
|
|
111
|
+
{ name: '/session', description: 'Show current session info (name, message count, context %)' },
|
|
112
|
+
{ name: '/clear', description: 'Clear current session history' },
|
|
113
|
+
{ name: '/compact', description: 'Summarise old history and keep only recent messages' },
|
|
114
|
+
{ name: '/stop', description: 'Interrupt the in-flight turn' },
|
|
115
|
+
{ name: '/restart', description: 'Graceful session restart' },
|
|
116
|
+
{ name: '/model', description: 'Show the current AI model' },
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
/**
|
|
121
|
+
* POST /api/v1/agents/:agentId/messages
|
|
122
|
+
*
|
|
123
|
+
* Send a message to an agent and receive its response synchronously.
|
|
124
|
+
* Body: { message: string, chat_id: string, session_id?: string }
|
|
125
|
+
*/
|
|
126
|
+
router.post('/v1/agents/:agentId/messages', auth, async (req, res) => {
|
|
127
|
+
const { agentId } = req.params;
|
|
128
|
+
const apiKey = req.apiKey;
|
|
129
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
130
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const runner = agentRunners.get(agentId);
|
|
134
|
+
if (!runner) {
|
|
135
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const body = req.body;
|
|
139
|
+
const { message, chat_id, session_id, stream, timeout_ms, media_files } = body;
|
|
140
|
+
if (!message || typeof message !== 'string' || !message.trim()) {
|
|
141
|
+
res.status(400).json({ error: 'message is required and must be a non-empty string' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (message.length > MAX_MESSAGE_LENGTH) {
|
|
145
|
+
res.status(400).json({ error: `message too long (max ${MAX_MESSAGE_LENGTH} characters)` });
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!chat_id || typeof chat_id !== 'string' || !chat_id.trim()) {
|
|
149
|
+
res.status(400).json({ error: 'chat_id is required and must be a non-empty string' });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(chat_id.trim())) {
|
|
153
|
+
res.status(400).json({ error: 'chat_id must be 1-64 alphanumeric characters, hyphens, or underscores' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (session_id !== undefined && typeof session_id !== 'string') {
|
|
157
|
+
res.status(400).json({ error: 'session_id must be a string if provided' });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Validate media_files
|
|
161
|
+
let validatedMediaFiles;
|
|
162
|
+
if (media_files !== undefined) {
|
|
163
|
+
if (!Array.isArray(media_files) || media_files.some((f) => typeof f !== 'string')) {
|
|
164
|
+
res.status(400).json({ error: 'media_files must be an array of strings' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (media_files.length > 5) {
|
|
168
|
+
res.status(400).json({ error: 'media_files exceeds maximum of 5 images per message' });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
// Validate each path is within agent media root (path traversal guard)
|
|
172
|
+
const routerAgentsBaseDir = runner.getAgentsBaseDir();
|
|
173
|
+
for (const f of media_files) {
|
|
174
|
+
try {
|
|
175
|
+
media_store_1.MediaStore.resolvePath(routerAgentsBaseDir, agentId, f);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
res.status(400).json({ error: `Invalid media path: ${f}` });
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
validatedMediaFiles = media_files;
|
|
183
|
+
}
|
|
184
|
+
const requestId = (0, crypto_1.randomUUID)();
|
|
185
|
+
const sessionId = session_id ?? (0, crypto_1.randomUUID)();
|
|
186
|
+
const chatIdStr = chat_id.trim();
|
|
187
|
+
const startTime = Date.now();
|
|
188
|
+
const timeoutMs = typeof timeout_ms === 'number' && timeout_ms > 0 && timeout_ms <= 600000
|
|
189
|
+
? timeout_ms
|
|
190
|
+
: DEFAULT_TIMEOUT_MS;
|
|
191
|
+
// Slash command dispatch — runs instead of sending to Claude
|
|
192
|
+
if (message.trim().startsWith('/')) {
|
|
193
|
+
try {
|
|
194
|
+
const result = await runner.executeApiCommand(sessionId, chatIdStr, message.trim());
|
|
195
|
+
res.json({ command: message.trim(), session_id: sessionId, result });
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
res.status(500).json({ error: err.message ?? 'Command failed' });
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (stream) {
|
|
203
|
+
// SSE streaming mode
|
|
204
|
+
let cleanup;
|
|
205
|
+
try {
|
|
206
|
+
const sseCallbacks = {
|
|
207
|
+
onChunk: (event) => {
|
|
208
|
+
try {
|
|
209
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
210
|
+
}
|
|
211
|
+
catch { /* client gone */ }
|
|
212
|
+
},
|
|
213
|
+
onDone: (fullText) => {
|
|
214
|
+
try {
|
|
215
|
+
res.write(`data: ${JSON.stringify({ type: 'result', text: fullText, request_id: requestId, session_id: sessionId, duration_ms: Date.now() - startTime })}\n\n`);
|
|
216
|
+
res.write('data: [DONE]\n\n');
|
|
217
|
+
res.end();
|
|
218
|
+
}
|
|
219
|
+
catch { /* client gone */ }
|
|
220
|
+
},
|
|
221
|
+
onError: (err) => {
|
|
222
|
+
try {
|
|
223
|
+
res.write(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`);
|
|
224
|
+
res.end();
|
|
225
|
+
}
|
|
226
|
+
catch { /* client gone */ }
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
// Preflight conflict check — return 409 JSON before SSE headers are sent
|
|
230
|
+
if (runner.hasActiveApiSession(sessionId)) {
|
|
231
|
+
res.status(409).json({ error: 'Session already has a pending request' });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
res.writeHead(200, {
|
|
235
|
+
'Content-Type': 'text/event-stream',
|
|
236
|
+
'Cache-Control': 'no-cache',
|
|
237
|
+
'Connection': 'keep-alive',
|
|
238
|
+
'X-Accel-Buffering': 'no',
|
|
239
|
+
});
|
|
240
|
+
res.flushHeaders();
|
|
241
|
+
res.socket?.setNoDelay(true);
|
|
242
|
+
const agentCfg = agentConfigs.get(agentId);
|
|
243
|
+
const allowTools = agentCfg.allow_tools ?? !!apiKey.allow_tools;
|
|
244
|
+
cleanup = await runner.sendApiMessageStream(sessionId, chatIdStr, message.trim(), sseCallbacks, { timeoutMs, allowTools, mediaFiles: validatedMediaFiles });
|
|
245
|
+
// Client disconnect -> cleanup
|
|
246
|
+
res.on('close', cleanup);
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
const code = err.code;
|
|
250
|
+
if (!res.headersSent) {
|
|
251
|
+
if (code === 'CONFLICT') {
|
|
252
|
+
res.status(409).json({ error: 'Session already has a pending request' });
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
res.status(500).json({ error: 'Internal error' });
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
try {
|
|
260
|
+
res.write(`data: ${JSON.stringify({ type: 'error', message: 'Internal error' })}\n\n`);
|
|
261
|
+
res.end();
|
|
262
|
+
}
|
|
263
|
+
catch { /* client gone */ }
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// Synchronous mode (existing behavior)
|
|
269
|
+
try {
|
|
270
|
+
const agentCfgSync = agentConfigs.get(agentId);
|
|
271
|
+
const allowToolsSync = agentCfgSync.allow_tools ?? !!apiKey.allow_tools;
|
|
272
|
+
const response = await runner.sendApiMessage(sessionId, chatIdStr, message.trim(), {
|
|
273
|
+
timeoutMs,
|
|
274
|
+
allowTools: allowToolsSync,
|
|
275
|
+
mediaFiles: validatedMediaFiles,
|
|
276
|
+
});
|
|
277
|
+
res.json({
|
|
278
|
+
request_id: requestId,
|
|
279
|
+
agent_id: agentId,
|
|
280
|
+
response,
|
|
281
|
+
session_id: sessionId,
|
|
282
|
+
duration_ms: Date.now() - startTime,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
catch (err) {
|
|
286
|
+
const code = err.code;
|
|
287
|
+
if (code === 'TIMEOUT') {
|
|
288
|
+
res.status(504).json({ error: 'Agent response timeout' });
|
|
289
|
+
}
|
|
290
|
+
else if (code === 'CONFLICT') {
|
|
291
|
+
res.status(409).json({ error: 'Session already has a pending request' });
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
res.status(500).json({ error: 'Internal error' });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
/**
|
|
300
|
+
* GET /api/v1/models
|
|
301
|
+
*
|
|
302
|
+
* List all supported Claude models from gateway config (falls back to defaults).
|
|
303
|
+
*/
|
|
304
|
+
router.get('/v1/models', auth, (_req, res) => {
|
|
305
|
+
res.json({ models: (models ?? []).map((m) => ({ id: m.id, name: m.label, alias: m.alias, contextWindow: m.contextWindow, multiplier: m.multiplier ?? 1 })) });
|
|
306
|
+
});
|
|
307
|
+
/**
|
|
308
|
+
* GET /api/v1/agents
|
|
309
|
+
*
|
|
310
|
+
* List agents scoped to the API key. Admin keys see all agents.
|
|
311
|
+
*/
|
|
312
|
+
router.get('/v1/agents', auth, (req, res) => {
|
|
313
|
+
const apiKey = req.apiKey;
|
|
314
|
+
const agents = [...agentConfigs.entries()]
|
|
315
|
+
.filter(([id]) => (0, auth_1.canAccessAgent)(apiKey, id))
|
|
316
|
+
.map(([id, cfg]) => ({
|
|
317
|
+
id,
|
|
318
|
+
description: cfg.description,
|
|
319
|
+
model: cfg.claude?.model ?? null,
|
|
320
|
+
allow_tools: cfg.allow_tools ?? false,
|
|
321
|
+
}));
|
|
322
|
+
res.json({ agents });
|
|
323
|
+
});
|
|
324
|
+
/**
|
|
325
|
+
* GET /api/v1/agents/sessions
|
|
326
|
+
*
|
|
327
|
+
* List all sessions across all agents. Admin only.
|
|
328
|
+
* Queries each agent's history DB sequentially and returns a nested agents → sessions structure.
|
|
329
|
+
*/
|
|
330
|
+
router.get('/v1/agents/sessions', auth, async (req, res) => {
|
|
331
|
+
const apiKey = req.apiKey;
|
|
332
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
333
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const agents = await Promise.all([...agentRunners.entries()].map(async ([agentId, runner]) => {
|
|
337
|
+
const cfg = agentConfigs.get(agentId);
|
|
338
|
+
const [sessions, nameMap] = await Promise.all([
|
|
339
|
+
Promise.resolve(runner.getHistoryDb().listSessions()),
|
|
340
|
+
runner.getAllSessionNames(),
|
|
341
|
+
]);
|
|
342
|
+
return {
|
|
343
|
+
agentId,
|
|
344
|
+
description: cfg?.description ?? '',
|
|
345
|
+
sessions: sessions.map((s) => ({ ...s, sessionName: nameMap.get(s.sessionId) ?? null })),
|
|
346
|
+
};
|
|
347
|
+
}));
|
|
348
|
+
res.json({ agents });
|
|
349
|
+
});
|
|
350
|
+
/**
|
|
351
|
+
* POST /api/v1/agents
|
|
352
|
+
*
|
|
353
|
+
* Create a new agent entry in config.json. Requires admin key.
|
|
354
|
+
* Body: { id, description, model? }
|
|
355
|
+
*/
|
|
356
|
+
router.post('/v1/agents', auth, async (req, res) => {
|
|
357
|
+
const apiKey = req.apiKey;
|
|
358
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
359
|
+
res.status(403).json({ error: 'Admin key required to create agents' });
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (!configPath) {
|
|
363
|
+
res.status(501).json({ error: 'Agent management not available (no configPath)' });
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
const body = req.body;
|
|
367
|
+
const { id, description, model } = body;
|
|
368
|
+
if (!id || typeof id !== 'string' || !AGENT_ID_RE.test(id)) {
|
|
369
|
+
res.status(400).json({ error: 'id must match pattern [a-z][a-z0-9_-]{1,31}' });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
if (!description || typeof description !== 'string' || !description.trim()) {
|
|
373
|
+
res.status(400).json({ error: 'description is required' });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (agentConfigs.has(id)) {
|
|
377
|
+
res.status(409).json({ error: `Agent '${id}' already exists` });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const workspace = path.join('~', '.claude-gateway', 'agents', id, 'workspace');
|
|
381
|
+
const workspaceAbs = path.join(os.homedir(), '.claude-gateway', 'agents', id, 'workspace');
|
|
382
|
+
const newAgent = {
|
|
383
|
+
id,
|
|
384
|
+
description: description.trim(),
|
|
385
|
+
workspace,
|
|
386
|
+
env: path.join('~', '.claude-gateway', 'agents', id, 'workspace', '.env'),
|
|
387
|
+
claude: {
|
|
388
|
+
model: typeof model === 'string' && model.trim() ? model.trim() : 'claude-sonnet-4-6',
|
|
389
|
+
dangerouslySkipPermissions: false,
|
|
390
|
+
extraFlags: [],
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
// Write config first — if this fails, no workspace is created (avoids orphaned directories).
|
|
394
|
+
try {
|
|
395
|
+
await writeAgentsToConfig(configPath, (agents) => agents.push(newAgent), id);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
const code = err.code;
|
|
399
|
+
if (code === 'DUPLICATE') {
|
|
400
|
+
res.status(409).json({ error: `Agent '${id}' already exists` });
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
res.status(500).json({ error: `Failed to write config: ${err.message}` });
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
// Config written successfully — now create workspace directory and stub files.
|
|
408
|
+
const stubFiles = {
|
|
409
|
+
'AGENTS.md': `# Agent: ${id}\n\n${description.trim()}\n`,
|
|
410
|
+
'SOUL.md': `# Soul\n\n`,
|
|
411
|
+
'USER.md': `# User Profile\n\n`,
|
|
412
|
+
'MEMORY.md': `# Memory\n\n`,
|
|
413
|
+
};
|
|
414
|
+
try {
|
|
415
|
+
fs.mkdirSync(workspaceAbs, { recursive: true });
|
|
416
|
+
for (const [filename, stub] of Object.entries(stubFiles)) {
|
|
417
|
+
const filePath = path.join(workspaceAbs, filename);
|
|
418
|
+
if (!fs.existsSync(filePath)) {
|
|
419
|
+
fs.writeFileSync(filePath, stub, 'utf8');
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
// Config was already written — log the workspace failure but still return 201.
|
|
425
|
+
// The agent is valid; workspace will be auto-created on next gateway start.
|
|
426
|
+
console.error(`[api] Warning: agent '${id}' created in config but workspace setup failed: ${err.message}`);
|
|
427
|
+
}
|
|
428
|
+
res.status(201).json({ agent: { id, description: newAgent.description, model: newAgent.claude.model } });
|
|
429
|
+
});
|
|
430
|
+
/**
|
|
431
|
+
* PATCH /api/v1/agents/:agentId
|
|
432
|
+
*
|
|
433
|
+
* Update agent description and/or model. Requires write access to the agent.
|
|
434
|
+
* Body: { description?, model? }
|
|
435
|
+
*/
|
|
436
|
+
router.patch('/v1/agents/:agentId', auth, async (req, res) => {
|
|
437
|
+
const apiKey = req.apiKey;
|
|
438
|
+
const { agentId } = req.params;
|
|
439
|
+
if (!(0, auth_1.canWriteAgent)(apiKey, agentId)) {
|
|
440
|
+
res.status(403).json({ error: 'Write permission required' });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
if (!configPath) {
|
|
444
|
+
res.status(501).json({ error: 'Agent management not available (no configPath)' });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (!agentConfigs.has(agentId)) {
|
|
448
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const body = req.body;
|
|
452
|
+
const { description, model, allow_tools } = body;
|
|
453
|
+
if (description !== undefined && (typeof description !== 'string' || !description.trim())) {
|
|
454
|
+
res.status(400).json({ error: 'description must be a non-empty string' });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (model !== undefined && (typeof model !== 'string' || !model.trim())) {
|
|
458
|
+
res.status(400).json({ error: 'model must be a non-empty string' });
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (allow_tools !== undefined && typeof allow_tools !== 'boolean') {
|
|
462
|
+
res.status(400).json({ error: 'allow_tools must be a boolean' });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
await writeAgentsToConfig(configPath, (agents) => {
|
|
467
|
+
const agent = agents.find((a) => a.id === agentId);
|
|
468
|
+
if (!agent)
|
|
469
|
+
return;
|
|
470
|
+
if (description !== undefined)
|
|
471
|
+
agent.description = description.trim();
|
|
472
|
+
if (model !== undefined) {
|
|
473
|
+
const claude = agent.claude;
|
|
474
|
+
if (claude)
|
|
475
|
+
claude.model = model.trim();
|
|
476
|
+
}
|
|
477
|
+
if (allow_tools !== undefined)
|
|
478
|
+
agent.allow_tools = allow_tools;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
catch (err) {
|
|
482
|
+
res.status(500).json({ error: `Failed to write config: ${err.message}` });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
// Sync in-memory map with what was written to disk
|
|
486
|
+
const cfg = agentConfigs.get(agentId);
|
|
487
|
+
if (description !== undefined)
|
|
488
|
+
cfg.description = description.trim();
|
|
489
|
+
if (model !== undefined && cfg.claude)
|
|
490
|
+
cfg.claude.model = model.trim();
|
|
491
|
+
if (allow_tools !== undefined)
|
|
492
|
+
cfg.allow_tools = allow_tools;
|
|
493
|
+
res.json({ agent: { id: agentId, description: cfg.description, model: cfg.claude?.model, allow_tools: cfg.allow_tools ?? false } });
|
|
494
|
+
});
|
|
495
|
+
/**
|
|
496
|
+
* DELETE /api/v1/agents/:agentId
|
|
497
|
+
*
|
|
498
|
+
* Remove agent from config.json and stop the running runner. Requires admin key.
|
|
499
|
+
*/
|
|
500
|
+
router.delete('/v1/agents/:agentId', auth, async (req, res) => {
|
|
501
|
+
const apiKey = req.apiKey;
|
|
502
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
503
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
if (!configPath) {
|
|
507
|
+
res.status(501).json({ error: 'Agent management not available (no configPath)' });
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
const { agentId } = req.params;
|
|
511
|
+
if (!agentConfigs.has(agentId)) {
|
|
512
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
try {
|
|
516
|
+
await writeAgentsToConfig(configPath, (agents) => {
|
|
517
|
+
const idx = agents.findIndex((a) => a.id === agentId);
|
|
518
|
+
if (idx !== -1)
|
|
519
|
+
agents.splice(idx, 1);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
res.status(500).json({ error: `Failed to write config: ${err.message}` });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
// Stop and remove the running runner so the agent no longer responds after deletion.
|
|
527
|
+
const runner = agentRunners.get(agentId);
|
|
528
|
+
if (runner) {
|
|
529
|
+
try {
|
|
530
|
+
await runner.stop();
|
|
531
|
+
}
|
|
532
|
+
catch { /* ignore stop errors */ }
|
|
533
|
+
agentRunners.delete(agentId);
|
|
534
|
+
db_1.HistoryDB.evictDir(runner.getAgentDir(), agentId);
|
|
535
|
+
}
|
|
536
|
+
agentConfigs.delete(agentId);
|
|
537
|
+
res.json({ success: true, id: agentId });
|
|
538
|
+
});
|
|
539
|
+
// ─── Chat History API ─────────────────────────────────────────────────────────
|
|
540
|
+
/**
|
|
541
|
+
* GET /api/v1/agents/:agentId/chats
|
|
542
|
+
* List all chats (across all channels) for an agent from the history DB.
|
|
543
|
+
*/
|
|
544
|
+
router.get('/v1/agents/:agentId/chats', auth, (req, res) => {
|
|
545
|
+
const { agentId } = req.params;
|
|
546
|
+
const apiKey = req.apiKey;
|
|
547
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
548
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
const runner = agentRunners.get(agentId);
|
|
552
|
+
if (!runner) {
|
|
553
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const chats = runner.getHistoryDb().listChats();
|
|
557
|
+
res.json({ chats });
|
|
558
|
+
});
|
|
559
|
+
/**
|
|
560
|
+
* GET /api/v1/agents/:agentId/chats/:chatId/sessions
|
|
561
|
+
* List sessions for a specific chat (delegated to SessionStore).
|
|
562
|
+
* chatId format: "telegram-{rawId}" | "discord-{rawId}"
|
|
563
|
+
*/
|
|
564
|
+
router.get('/v1/agents/:agentId/chats/:chatId/sessions', auth, async (req, res) => {
|
|
565
|
+
const { agentId, chatId } = req.params;
|
|
566
|
+
const apiKey = req.apiKey;
|
|
567
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
568
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const runner = agentRunners.get(agentId);
|
|
572
|
+
if (!runner) {
|
|
573
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const { source, rawChatId } = parseHistoryChatId(chatId);
|
|
577
|
+
if (source !== 'telegram' && source !== 'discord') {
|
|
578
|
+
res.status(400).json({ error: 'Sessions endpoint only supports telegram/discord chats' });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const index = await runner.listSessionsForChat(rawChatId, source);
|
|
583
|
+
res.json(index);
|
|
584
|
+
}
|
|
585
|
+
catch (err) {
|
|
586
|
+
res.status(500).json({ error: err.message });
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
/**
|
|
590
|
+
* GET /api/v1/agents/:agentId/chats/:chatId/messages
|
|
591
|
+
* Paginated message history (cursor-based).
|
|
592
|
+
* Query: limit, before (ts ms), after (ts ms), session_id
|
|
593
|
+
*/
|
|
594
|
+
router.get('/v1/agents/:agentId/chats/:chatId/messages', auth, (req, res) => {
|
|
595
|
+
const { agentId, chatId } = req.params;
|
|
596
|
+
const apiKey = req.apiKey;
|
|
597
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
598
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const runner = agentRunners.get(agentId);
|
|
602
|
+
if (!runner) {
|
|
603
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const query = req.query;
|
|
607
|
+
const limit = query['limit'] ? Math.min(parseInt(query['limit'], 10) || 50, 200) : 50;
|
|
608
|
+
const before = query['before'] ? parseInt(query['before'], 10) : undefined;
|
|
609
|
+
const after = query['after'] ? parseInt(query['after'], 10) : undefined;
|
|
610
|
+
const sessionId = query['session_id'] ?? undefined;
|
|
611
|
+
const page = runner.getHistoryDb().getMessages(chatId, { limit, before, after, sessionId });
|
|
612
|
+
res.json(page);
|
|
613
|
+
});
|
|
614
|
+
/**
|
|
615
|
+
* GET /api/v1/agents/:agentId/chats/:chatId/messages/search
|
|
616
|
+
* Full-text search using SQLite FTS5.
|
|
617
|
+
* Query: q, limit, offset
|
|
618
|
+
*/
|
|
619
|
+
router.get('/v1/agents/:agentId/chats/:chatId/messages/search', auth, (req, res) => {
|
|
620
|
+
const { agentId, chatId } = req.params;
|
|
621
|
+
const apiKey = req.apiKey;
|
|
622
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
623
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const runner = agentRunners.get(agentId);
|
|
627
|
+
if (!runner) {
|
|
628
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const query = req.query;
|
|
632
|
+
const q = (query['q'] ?? '').trim();
|
|
633
|
+
if (!q) {
|
|
634
|
+
res.status(400).json({ error: 'q is required' });
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const limit = query['limit'] ? Math.min(parseInt(query['limit'], 10) || 20, 100) : 20;
|
|
638
|
+
const offset = query['offset'] ? parseInt(query['offset'], 10) : 0;
|
|
639
|
+
const page = runner.getHistoryDb().searchMessages(chatId, q, { limit, offset });
|
|
640
|
+
res.json(page);
|
|
641
|
+
});
|
|
642
|
+
/**
|
|
643
|
+
* POST /api/v1/agents/:agentId/chats/:chatId/sessions/:sessionId/messages
|
|
644
|
+
* Inject a message into an existing channel session (cross-channel continuation).
|
|
645
|
+
* Streams the assistant response as SSE.
|
|
646
|
+
* Body: { content: string, senderName?: string }
|
|
647
|
+
*/
|
|
648
|
+
router.post('/v1/agents/:agentId/chats/:chatId/sessions/:sessionId/messages', auth, async (req, res) => {
|
|
649
|
+
const { agentId, chatId, sessionId } = req.params;
|
|
650
|
+
const apiKey = req.apiKey;
|
|
651
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
652
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const runner = agentRunners.get(agentId);
|
|
656
|
+
if (!runner) {
|
|
657
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const { source, rawChatId } = parseHistoryChatId(chatId);
|
|
661
|
+
if (source !== 'telegram' && source !== 'discord') {
|
|
662
|
+
res.status(400).json({ error: 'Cross-channel messaging only supported for telegram/discord chats' });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
const body = req.body;
|
|
666
|
+
const content = body.content;
|
|
667
|
+
if (!content || typeof content !== 'string' || !content.trim()) {
|
|
668
|
+
res.status(400).json({ error: 'content is required and must be a non-empty string' });
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (content.length > MAX_MESSAGE_LENGTH) {
|
|
672
|
+
res.status(400).json({ error: `content too long (max ${MAX_MESSAGE_LENGTH} characters)` });
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const senderName = typeof body.senderName === 'string' ? body.senderName : undefined;
|
|
676
|
+
let cleanup;
|
|
677
|
+
try {
|
|
678
|
+
const sseCallbacks = {
|
|
679
|
+
onChunk: (event) => {
|
|
680
|
+
try {
|
|
681
|
+
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
682
|
+
}
|
|
683
|
+
catch { /* client gone */ }
|
|
684
|
+
},
|
|
685
|
+
onDone: (fullText) => {
|
|
686
|
+
try {
|
|
687
|
+
res.write(`data: ${JSON.stringify({ type: 'result', text: fullText, session_id: sessionId })}\n\n`);
|
|
688
|
+
res.write('data: [DONE]\n\n');
|
|
689
|
+
res.end();
|
|
690
|
+
}
|
|
691
|
+
catch { /* client gone */ }
|
|
692
|
+
},
|
|
693
|
+
onError: (err) => {
|
|
694
|
+
try {
|
|
695
|
+
res.write(`data: ${JSON.stringify({ type: 'error', message: err.message })}\n\n`);
|
|
696
|
+
res.end();
|
|
697
|
+
}
|
|
698
|
+
catch { /* client gone */ }
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
res.writeHead(200, {
|
|
702
|
+
'Content-Type': 'text/event-stream',
|
|
703
|
+
'Cache-Control': 'no-cache',
|
|
704
|
+
'Connection': 'keep-alive',
|
|
705
|
+
'X-Accel-Buffering': 'no',
|
|
706
|
+
});
|
|
707
|
+
res.flushHeaders();
|
|
708
|
+
res.socket?.setNoDelay(true);
|
|
709
|
+
cleanup = await runner.sendMessageToSession(rawChatId, source, sessionId, content.trim(), senderName, sseCallbacks, { timeoutMs: DEFAULT_TIMEOUT_MS });
|
|
710
|
+
res.on('close', cleanup);
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
if (!res.headersSent) {
|
|
714
|
+
res.status(500).json({ error: 'Internal error' });
|
|
715
|
+
}
|
|
716
|
+
else {
|
|
717
|
+
try {
|
|
718
|
+
res.write(`data: ${JSON.stringify({ type: 'error', message: 'Internal error' })}\n\n`);
|
|
719
|
+
res.end();
|
|
720
|
+
}
|
|
721
|
+
catch { /* client gone */ }
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
/**
|
|
726
|
+
* POST /api/v1/agents/:agentId/media
|
|
727
|
+
* Upload a media file as raw binary body (image/* or application/pdf).
|
|
728
|
+
* Headers: Content-Type (mime type), X-Filename (optional original filename)
|
|
729
|
+
* Body: raw file bytes
|
|
730
|
+
* Returns: { mediaPath: string } — relative path usable in message mediaFiles[]
|
|
731
|
+
*/
|
|
732
|
+
router.post('/v1/agents/:agentId/media', auth, (req, res, next) => {
|
|
733
|
+
// Buffer raw body up to maxUploadBytes; express.json/urlencoded don't handle binary
|
|
734
|
+
const mimeType = (req.headers['content-type'] ?? '').split(';')[0].trim();
|
|
735
|
+
if (!media_store_1.MediaStore.isAllowedMime(mimeType)) {
|
|
736
|
+
res.status(415).json({ error: 'Unsupported file type. Allowed: image/*, application/pdf' });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const chunks = [];
|
|
740
|
+
let size = 0;
|
|
741
|
+
req.on('data', (chunk) => {
|
|
742
|
+
size += chunk.length;
|
|
743
|
+
if (size > media_store_1.MediaStore.maxUploadBytes) {
|
|
744
|
+
if (!res.headersSent)
|
|
745
|
+
res.status(413).json({ error: `File too large (max ${media_store_1.MediaStore.maxUploadBytes / 1024 / 1024}MB)` });
|
|
746
|
+
req.destroy();
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
chunks.push(chunk);
|
|
750
|
+
});
|
|
751
|
+
req.on('end', () => {
|
|
752
|
+
req.rawFileBuffer = Buffer.concat(chunks);
|
|
753
|
+
req.rawFileMime = mimeType;
|
|
754
|
+
next();
|
|
755
|
+
});
|
|
756
|
+
req.on('error', () => {
|
|
757
|
+
if (!res.headersSent)
|
|
758
|
+
res.status(500).json({ error: 'Upload stream error' });
|
|
759
|
+
});
|
|
760
|
+
}, async (req, res) => {
|
|
761
|
+
const { agentId } = req.params;
|
|
762
|
+
const apiKey = req.apiKey;
|
|
763
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
764
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
const runner = agentRunners.get(agentId);
|
|
768
|
+
if (!runner) {
|
|
769
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
// Rate limit: max 20 uploads/min per API key
|
|
773
|
+
const keyHash = (0, crypto_1.createHash)('sha256').update(apiKey.key).digest('hex').slice(0, 16);
|
|
774
|
+
if (!checkUploadRateLimit(keyHash)) {
|
|
775
|
+
res.status(429).json({ error: 'Too many uploads. Limit: 20 per minute.' });
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
const buf = req.rawFileBuffer;
|
|
779
|
+
const mimeType = req.rawFileMime ?? '';
|
|
780
|
+
if (!buf || buf.length === 0) {
|
|
781
|
+
res.status(400).json({ error: 'No file body received' });
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
// X-Filename: validate length, strip to basename, allow only safe characters
|
|
785
|
+
const rawFilename = req.headers['x-filename'] ?? 'upload';
|
|
786
|
+
if (rawFilename.length > 255) {
|
|
787
|
+
res.status(400).json({ error: 'X-Filename too long (max 255 chars)' });
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const baseName = path.basename(rawFilename).replace(/\s+/g, '_');
|
|
791
|
+
const safeBaseName = SAFE_FILENAME_RE.test(baseName) ? baseName : 'upload';
|
|
792
|
+
const rawExt = path.extname(safeBaseName).replace(/[^a-zA-Z0-9.]/g, '').slice(0, 10);
|
|
793
|
+
const ext = rawExt || (mimeType.includes('pdf') ? '.pdf' : '.bin');
|
|
794
|
+
const tmpFile = path.join(os.tmpdir(), `gw-${Date.now()}${ext}`);
|
|
795
|
+
try {
|
|
796
|
+
await fsp.writeFile(tmpFile, buf);
|
|
797
|
+
const agentsBaseDir = runner.getAgentsBaseDir();
|
|
798
|
+
// Store under ui-upload/{keyId}/ so each API key's uploads are isolated
|
|
799
|
+
const keySubdir = `ui-upload/${apiKeyId(apiKey.key)}`;
|
|
800
|
+
const mediaPath = media_store_1.MediaStore.copyToMedia(agentsBaseDir, agentId, keySubdir, tmpFile);
|
|
801
|
+
res.json({ mediaPath });
|
|
802
|
+
}
|
|
803
|
+
catch (err) {
|
|
804
|
+
res.status(500).json({ error: `Upload failed: ${err.message}` });
|
|
805
|
+
}
|
|
806
|
+
finally {
|
|
807
|
+
fsp.unlink(tmpFile).catch(() => { });
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
/**
|
|
811
|
+
* GET /api/v1/agents/:agentId/media/*filepath
|
|
812
|
+
* Serve a media file. Validates path stays within agent's media directory.
|
|
813
|
+
*/
|
|
814
|
+
router.get('/v1/agents/:agentId/media/*', auth, (req, res) => {
|
|
815
|
+
const { agentId } = req.params;
|
|
816
|
+
const apiKey = req.apiKey;
|
|
817
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
818
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
const runner = agentRunners.get(agentId);
|
|
822
|
+
if (!runner) {
|
|
823
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const wildcardParam = req.params['0'] ?? '';
|
|
827
|
+
const agentsBaseDir = runner.getAgentsBaseDir();
|
|
828
|
+
let absPath;
|
|
829
|
+
try {
|
|
830
|
+
absPath = media_store_1.MediaStore.resolvePath(agentsBaseDir, agentId, wildcardParam);
|
|
831
|
+
}
|
|
832
|
+
catch {
|
|
833
|
+
res.status(400).json({ error: 'Invalid path' });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (!fs.existsSync(absPath)) {
|
|
837
|
+
res.status(404).json({ error: 'Not found' });
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
// Cache media files for 7 days — content is immutable once written
|
|
841
|
+
res.setHeader('Cache-Control', 'private, max-age=604800, immutable');
|
|
842
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
843
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
844
|
+
res.sendFile(absPath);
|
|
845
|
+
});
|
|
846
|
+
// Schedule hourly cleanup of stale staging uploads (files older than 24h)
|
|
847
|
+
setInterval(() => {
|
|
848
|
+
const firstRunner = agentRunners.values().next().value;
|
|
849
|
+
if (firstRunner) {
|
|
850
|
+
try {
|
|
851
|
+
media_store_1.MediaStore.cleanupStaging(firstRunner.getAgentsBaseDir());
|
|
852
|
+
}
|
|
853
|
+
catch { /* non-critical */ }
|
|
854
|
+
}
|
|
855
|
+
}, 60 * 60 * 1000).unref(); // unref so cleanup timer doesn't keep process alive
|
|
856
|
+
// ──────────────────────────────────────────────────────────────
|
|
857
|
+
// Model management
|
|
858
|
+
// ──────────────────────────────────────────────────────────────
|
|
859
|
+
/**
|
|
860
|
+
* PUT /api/v1/agents/:agentId/model
|
|
861
|
+
*
|
|
862
|
+
* Set the active model for an agent. Persists to config.json.
|
|
863
|
+
*/
|
|
864
|
+
router.put('/v1/agents/:agentId/model', auth, async (req, res) => {
|
|
865
|
+
const { agentId } = req.params;
|
|
866
|
+
const apiKey = req.apiKey;
|
|
867
|
+
if (!(0, auth_1.isAdmin)(apiKey)) {
|
|
868
|
+
res.status(403).json({ error: 'Admin key required' });
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
const runner = agentRunners.get(agentId);
|
|
872
|
+
if (!runner) {
|
|
873
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const body = req.body;
|
|
877
|
+
const { model: newModel } = body;
|
|
878
|
+
if (!newModel || typeof newModel !== 'string') {
|
|
879
|
+
res.status(400).json({ error: 'model is required' });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
try {
|
|
883
|
+
await runner.setModel(newModel);
|
|
884
|
+
res.json({ model: newModel });
|
|
885
|
+
}
|
|
886
|
+
catch (err) {
|
|
887
|
+
const code = err.code;
|
|
888
|
+
if (code === 'UNKNOWN_MODEL') {
|
|
889
|
+
res.status(400).json({ error: `Unknown model: ${newModel}` });
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
res.status(500).json({ error: 'Failed to set model' });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
});
|
|
896
|
+
// ──────────────────────────────────────────────────────────────
|
|
897
|
+
// API Session management (/v1/agents/:agentId/sessions/...)
|
|
898
|
+
// ──────────────────────────────────────────────────────────────
|
|
899
|
+
function resolveApiSession(req, res) {
|
|
900
|
+
const { agentId } = req.params;
|
|
901
|
+
const apiKey = req.apiKey;
|
|
902
|
+
if (!(0, auth_1.canAccessAgent)(apiKey, agentId)) {
|
|
903
|
+
res.status(403).json({ error: `API key has no access to agent '${agentId}'` });
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
const runner = agentRunners.get(agentId);
|
|
907
|
+
if (!runner) {
|
|
908
|
+
res.status(404).json({ error: `Agent '${agentId}' not found` });
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
const chatId = (req.query['chat_id'] ?? req.body?.['chat_id']);
|
|
912
|
+
if (!chatId || typeof chatId !== 'string' || !chatId.trim()) {
|
|
913
|
+
res.status(400).json({ error: 'chat_id is required' });
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
if (!/^[a-zA-Z0-9_-]{1,64}$/.test(chatId.trim())) {
|
|
917
|
+
res.status(400).json({ error: 'chat_id must be 1-64 alphanumeric characters, hyphens, or underscores' });
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
return { runner, agentId, chatId: chatId.trim() };
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* GET /api/v1/agents/:agentId/sessions
|
|
924
|
+
* List API sessions for a chat_id.
|
|
925
|
+
*/
|
|
926
|
+
router.get('/v1/agents/:agentId/sessions', auth, async (req, res) => {
|
|
927
|
+
const ctx = resolveApiSession(req, res);
|
|
928
|
+
if (!ctx)
|
|
929
|
+
return;
|
|
930
|
+
const { runner, agentId, chatId } = ctx;
|
|
931
|
+
try {
|
|
932
|
+
const index = await runner.listApiSessions(chatId);
|
|
933
|
+
res.json(index);
|
|
934
|
+
}
|
|
935
|
+
catch (err) {
|
|
936
|
+
res.status(500).json({ error: err.message });
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
/**
|
|
940
|
+
* POST /api/v1/agents/:agentId/sessions
|
|
941
|
+
* Create a new API session. Optionally auto-names from a prompt.
|
|
942
|
+
*/
|
|
943
|
+
router.post('/v1/agents/:agentId/sessions', auth, async (req, res) => {
|
|
944
|
+
const ctx = resolveApiSession(req, res);
|
|
945
|
+
if (!ctx)
|
|
946
|
+
return;
|
|
947
|
+
const { runner, agentId, chatId } = ctx;
|
|
948
|
+
const body = req.body;
|
|
949
|
+
const promptText = typeof body.prompt === 'string' ? body.prompt.trim() : undefined;
|
|
950
|
+
const explicitName = typeof body.name === 'string' ? body.name.trim() : undefined;
|
|
951
|
+
try {
|
|
952
|
+
const meta = await runner.createApiSession(chatId, promptText, explicitName);
|
|
953
|
+
res.status(201).json({ sessionId: meta.id, sessionName: meta.name, createdAt: meta.createdAt });
|
|
954
|
+
}
|
|
955
|
+
catch (err) {
|
|
956
|
+
res.status(500).json({ error: err.message });
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
/**
|
|
960
|
+
* GET /api/v1/agents/:agentId/sessions/:sessionId/info
|
|
961
|
+
*/
|
|
962
|
+
router.get('/v1/agents/:agentId/sessions/:sessionId/info', auth, async (req, res) => {
|
|
963
|
+
const ctx = resolveApiSession(req, res);
|
|
964
|
+
if (!ctx)
|
|
965
|
+
return;
|
|
966
|
+
const { runner, chatId } = ctx;
|
|
967
|
+
const { sessionId } = req.params;
|
|
968
|
+
try {
|
|
969
|
+
const info = await runner.getApiSessionInfo(chatId, sessionId);
|
|
970
|
+
if (!info) {
|
|
971
|
+
res.status(404).json({ error: 'Session not found' });
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
res.json(info);
|
|
975
|
+
}
|
|
976
|
+
catch (err) {
|
|
977
|
+
res.status(500).json({ error: err.message });
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
/**
|
|
981
|
+
* PATCH /api/v1/agents/:agentId/sessions/:sessionId
|
|
982
|
+
* Rename a session.
|
|
983
|
+
*/
|
|
984
|
+
router.patch('/v1/agents/:agentId/sessions/:sessionId', auth, async (req, res) => {
|
|
985
|
+
const ctx = resolveApiSession(req, res);
|
|
986
|
+
if (!ctx)
|
|
987
|
+
return;
|
|
988
|
+
const { runner, chatId } = ctx;
|
|
989
|
+
const { sessionId } = req.params;
|
|
990
|
+
const body = req.body;
|
|
991
|
+
const sessionName = typeof body.sessionName === 'string' ? body.sessionName.trim() : undefined;
|
|
992
|
+
if (!sessionName) {
|
|
993
|
+
res.status(400).json({ error: 'sessionName is required' });
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
try {
|
|
997
|
+
await runner.renameApiSession(chatId, sessionId, sessionName);
|
|
998
|
+
res.json({ sessionId, sessionName });
|
|
999
|
+
}
|
|
1000
|
+
catch (err) {
|
|
1001
|
+
res.status(500).json({ error: err.message });
|
|
1002
|
+
}
|
|
1003
|
+
});
|
|
1004
|
+
/**
|
|
1005
|
+
* DELETE /api/v1/agents/:agentId/sessions/:sessionId
|
|
1006
|
+
*/
|
|
1007
|
+
router.delete('/v1/agents/:agentId/sessions/:sessionId', auth, async (req, res) => {
|
|
1008
|
+
const ctx = resolveApiSession(req, res);
|
|
1009
|
+
if (!ctx)
|
|
1010
|
+
return;
|
|
1011
|
+
const { runner, chatId } = ctx;
|
|
1012
|
+
const { sessionId } = req.params;
|
|
1013
|
+
try {
|
|
1014
|
+
await runner.deleteApiSession(chatId, sessionId);
|
|
1015
|
+
res.status(204).send();
|
|
1016
|
+
}
|
|
1017
|
+
catch (err) {
|
|
1018
|
+
res.status(500).json({ error: err.message });
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
/**
|
|
1022
|
+
* POST /api/v1/agents/:agentId/sessions/:sessionId/clear
|
|
1023
|
+
*/
|
|
1024
|
+
router.post('/v1/agents/:agentId/sessions/:sessionId/clear', auth, async (req, res) => {
|
|
1025
|
+
const ctx = resolveApiSession(req, res);
|
|
1026
|
+
if (!ctx)
|
|
1027
|
+
return;
|
|
1028
|
+
const { runner, chatId } = ctx;
|
|
1029
|
+
const { sessionId } = req.params;
|
|
1030
|
+
try {
|
|
1031
|
+
const result = await runner.executeApiCommand(sessionId, chatId, '/clear');
|
|
1032
|
+
res.json(result);
|
|
1033
|
+
}
|
|
1034
|
+
catch (err) {
|
|
1035
|
+
res.status(500).json({ error: err.message });
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
/**
|
|
1039
|
+
* POST /api/v1/agents/:agentId/sessions/:sessionId/compact
|
|
1040
|
+
*/
|
|
1041
|
+
router.post('/v1/agents/:agentId/sessions/:sessionId/compact', auth, async (req, res) => {
|
|
1042
|
+
const ctx = resolveApiSession(req, res);
|
|
1043
|
+
if (!ctx)
|
|
1044
|
+
return;
|
|
1045
|
+
const { runner, chatId } = ctx;
|
|
1046
|
+
const { sessionId } = req.params;
|
|
1047
|
+
try {
|
|
1048
|
+
const result = await runner.executeApiCommand(sessionId, chatId, '/compact');
|
|
1049
|
+
res.json(result);
|
|
1050
|
+
}
|
|
1051
|
+
catch (err) {
|
|
1052
|
+
res.status(500).json({ error: err.message });
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
/**
|
|
1056
|
+
* POST /api/v1/agents/:agentId/sessions/:sessionId/stop
|
|
1057
|
+
*/
|
|
1058
|
+
router.post('/v1/agents/:agentId/sessions/:sessionId/stop', auth, async (req, res) => {
|
|
1059
|
+
const ctx = resolveApiSession(req, res);
|
|
1060
|
+
if (!ctx)
|
|
1061
|
+
return;
|
|
1062
|
+
const { runner, chatId } = ctx;
|
|
1063
|
+
const { sessionId } = req.params;
|
|
1064
|
+
try {
|
|
1065
|
+
const result = await runner.executeApiCommand(sessionId, chatId, '/stop');
|
|
1066
|
+
res.json(result);
|
|
1067
|
+
}
|
|
1068
|
+
catch (err) {
|
|
1069
|
+
res.status(500).json({ error: err.message });
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
/**
|
|
1073
|
+
* POST /api/v1/agents/:agentId/sessions/:sessionId/restart
|
|
1074
|
+
*/
|
|
1075
|
+
router.post('/v1/agents/:agentId/sessions/:sessionId/restart', auth, async (req, res) => {
|
|
1076
|
+
const ctx = resolveApiSession(req, res);
|
|
1077
|
+
if (!ctx)
|
|
1078
|
+
return;
|
|
1079
|
+
const { runner, chatId } = ctx;
|
|
1080
|
+
const { sessionId } = req.params;
|
|
1081
|
+
try {
|
|
1082
|
+
const result = await runner.executeApiCommand(sessionId, chatId, '/restart');
|
|
1083
|
+
res.json(result);
|
|
1084
|
+
}
|
|
1085
|
+
catch (err) {
|
|
1086
|
+
res.status(500).json({ error: err.message });
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
return router;
|
|
1090
|
+
}
|
|
1091
|
+
function parseHistoryChatId(fullChatId) {
|
|
1092
|
+
if (fullChatId.startsWith('telegram-'))
|
|
1093
|
+
return { source: 'telegram', rawChatId: fullChatId.slice(9) };
|
|
1094
|
+
if (fullChatId.startsWith('discord-'))
|
|
1095
|
+
return { source: 'discord', rawChatId: fullChatId.slice(8) };
|
|
1096
|
+
return { source: 'api', rawChatId: fullChatId };
|
|
1097
|
+
}
|
|
1098
|
+
//# sourceMappingURL=router.js.map
|