Spaces:
Build error
Build error
File size: 10,908 Bytes
5348690 3e56aaa 5348690 3e56aaa 5348690 3e56aaa 5348690 3e56aaa 5348690 3e56aaa 5348690 3e56aaa 5348690 3e56aaa 5348690 3e56aaa 5348690 3e56aaa 5348690 3e56aaa 5348690 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 | import express from 'express';
import cors from 'cors';
import { handleMessage, clearConversationHistory, getHistory, listSessions, createSession, switchSession, deleteSession, getCurrentSessionId, getAndClearNotifications } from './agent.js';
import { executeListScheduledTasks } from './tools/cron.js';
import { executeReadFile, executeWriteFile } from './tools/fs.js';
import { TimerModel, CronModel, MemoryModel } from './memory/db.js';
import { getProvider, setProvider, listProviders } from './llm.js';
import { webhookCallback } from 'grammy';
import { bot } from './bot.js';
import { env } from './config.js';
import { pushNotification } from './agent.js';
// @ts-ignore
import cronparser from 'cron-parser';
const app = express();
app.use(cors());
app.use(express.json());
// --- AUTHENTICATION MIDDLEWARE ---
app.use((req, res, next) => {
// Skip auth for the Telegram webhook (Telegram authenticates via the bot token in the URL inherently)
if (req.path === '/api/telegram-webhook') return next();
const expectedToken = env.API_BEARER_TOKEN;
if (!expectedToken) return next(); // If no token is configured, allow all (Local Dev)
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Unauthorized: Missing or invalid Bearer token' });
}
const token = authHeader.split(' ')[1];
if (token !== expectedToken) {
return res.status(403).json({ error: 'Forbidden: Incorrect Token' });
}
next();
});
export function startApi() {
const port = process.env.PORT || 7860;
// --- CHAT MESSAGING ---
app.post('/api/chat', async (req, res) => {
const { message, model } = req.body;
if (!message) {
return res.status(400).json({ error: "Message is required" });
}
// Set headers for Server-Sent Events (SSE)
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const sendEvent = (type: string, data: any) => {
res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
};
const startTime = Date.now();
sendEvent('start', { status: 'Processing...' });
try {
const finalResponse = await handleMessage(
message,
(toolName, args) => {
sendEvent('tool_start', { name: toolName, args });
},
(toolName, result) => {
sendEvent('tool_end', { name: toolName, result });
},
false,
model
);
const executionTimeMs = Date.now() - startTime;
const approximateTokens = Math.floor((message.length + finalResponse.length) / 4);
sendEvent('done', {
response: finalResponse,
metadata: {
executionTimeMs,
tokens: approximateTokens,
cost: `$0.00`
}
});
res.end();
} catch (error: any) {
console.error("API Error:", error);
sendEvent('error', { message: error.message });
res.end();
}
});
// --- SESSION MANAGEMENT (ChatGPT-style) ---
// List all chat sessions
app.get('/api/chat/list', (req, res) => {
const sessions = listSessions();
const activeId = getCurrentSessionId();
res.json({ success: true, sessions, activeSessionId: activeId });
});
// Create a new chat session
app.post('/api/chat/new', (req, res) => {
const session = createSession();
res.json({ success: true, session });
});
// Switch to a specific session
app.post('/api/chat/switch', (req, res) => {
const { id } = req.body;
if (!id) return res.status(400).json({ error: "Missing session id" });
const ok = switchSession(id);
if (!ok) return res.status(404).json({ error: "Session not found" });
const history = getHistory().filter(msg => msg.role !== 'system');
res.json({ success: true, history });
});
// Delete a session
app.delete('/api/chat/:id', (req, res) => {
const { id } = req.params;
const ok = deleteSession(id);
if (!ok) return res.status(404).json({ error: "Session not found" });
res.json({ success: true });
});
// Clear chat = create new session (old one stays archived)
app.post('/api/chat/clear', (req, res) => {
clearConversationHistory();
res.json({ success: true, message: "New chat created", sessionId: getCurrentSessionId() });
});
// Get history of active session
app.get('/api/chat/history', (req, res) => {
const history = getHistory().filter(msg => msg.role !== 'system');
const activeId = getCurrentSessionId();
res.json({ success: true, history, activeSessionId: activeId });
});
// --- NOTIFICATIONS INBOX ---
app.get('/api/notifications', (req, res) => {
const notifications = getAndClearNotifications();
res.json({ success: true, notifications });
});
// --- SYSTEM ENDPOINTS ---
// Get active cron jobs
app.get('/api/system/cron', async (req, res) => {
const jobs = await executeListScheduledTasks();
res.json({ success: true, text: jobs });
});
// Read a specific file (e.g. heartbeat.md, soul.md, memory)
app.get('/api/system/file', async (req, res) => {
const filePath = req.query.path as string;
if (!filePath) return res.status(400).json({ error: "Missing path" });
if (filePath.includes('memory/db.json') || filePath === 'memory') {
try {
const rows = await MemoryModel.find().sort({ timestamp: -1 }).lean();
if (rows.length === 0) {
return res.json({ success: true, content: '🧠 No long-term memories saved yet.' });
}
const formatted = rows.map((r: any) => `[${r.timestamp}] - ${r.content}`).join('\n\n');
return res.json({ success: true, content: `🧠 Long-Term Memory:\n\n${formatted}` });
} catch (e: any) {
return res.status(500).json({ error: `DB Error: ${e.message}` });
}
}
try {
const content = await executeReadFile(filePath);
if (content.includes('Error')) {
res.json({ success: true, content: `Das Dokument '${filePath}' wurde auf dem Cloud-Server noch nicht generiert.\n\nDer Agent wird es bald automatisch erstellen.` });
} else {
res.json({ success: true, content });
}
} catch (e: any) {
res.json({ success: false, content: `Error processing request: ${e.message}` });
}
});
// Write a specific file
app.post('/api/system/file', async (req, res) => {
const { path, content } = req.body;
if (!path || content === undefined) return res.status(400).json({ error: "Missing path or content" });
const result = await executeWriteFile(path, content);
res.json({ success: true, result });
});
// --- LLM PROVIDER SWITCHING ---
app.get('/api/providers', (req, res) => {
res.json({ current: getProvider(), available: listProviders() });
});
app.post('/api/provider', (req, res) => {
const { provider } = req.body;
if (!provider) return res.status(400).json({ error: 'Missing provider' });
const available = listProviders();
if (!available.includes(provider)) return res.status(400).json({ error: `Unknown provider. Available: ${available.join(', ')}` });
setProvider(provider);
res.json({ success: true, provider });
});
// --- TELEGRAM WEBHOOK ---
// Instead of polling, Telegram will POST to this endpoint
const webhookUrl = process.env.WEBHOOK_URL;
if (webhookUrl) {
app.use('/api/telegram-webhook', webhookCallback(bot, 'express'));
}
// --- EXTERNAL CRON TICK ---
// Called e.g. every 5 minutes by cron-job.org
app.post('/api/cron/tick', async (req, res) => {
const nowMs = Date.now();
const nowIso = new Date(nowMs).toISOString();
let executedLogs: string[] = [];
try {
// 1. Process Due Timers (One-Time)
const dueTimers = await TimerModel.find({ triggerAt: { $lte: nowIso } }).lean();
for (const t of dueTimers) {
try {
await bot.api.sendMessage(env.TELEGRAM_USER_ID, `⏰ **Einmalige Erinnerung:**\n\n${(t as any).message}`, { parse_mode: 'Markdown' });
pushNotification('timer', `⏰ Einmalige Erinnerung: ${(t as any).message}`);
executedLogs.push(`Executed Timer: ${(t as any).id}`);
} catch (e: any) {
console.error(`Timer ${(t as any).id} failed:`, e);
}
// Always delete after attempting execution
await TimerModel.deleteOne({ id: (t as any).id });
}
// 2. Process Cron Jobs (Repeating)
// Note: Since cron triggers exactly at the minute, and the 'tick' might be called every 5 minutes,
// we should check if the next trigger time falls within the LAST 5 minutes.
const allCrons = await CronModel.find().lean();
for (const c of allCrons) {
try {
const interval = (cronparser as any).parseExpression((c as any).cronExpression);
const prevDateMs = interval.prev().getTime();
// If the job should have run in the last ~10 mins (600000ms buffer) and we are ticking now
if (nowMs - prevDateMs < 600000 && nowMs > prevDateMs) {
await bot.api.sendMessage(env.TELEGRAM_USER_ID, `⏰ **Wiederkehrender Cronjob:**\n\n${(c as any).message}`, { parse_mode: 'Markdown' });
pushNotification('cron', `⏰ Wiederkehrender Cronjob: ${(c as any).message}`);
executedLogs.push(`Executed Cron: ${(c as any).id}`);
}
} catch (e) {
console.error(`Cron ${(c as any).id} failed evaluation:`, e);
}
}
res.json({ success: true, processed: executedLogs });
} catch (e: any) {
console.error("Cron tick error:", e);
res.status(500).json({ error: e.message });
}
});
app.listen(Number(port), '0.0.0.0', () => {
console.log(`🚀 API Server running on 0.0.0.0:${port} (Accessible via Tailscale VPN)`);
});
}
|